Skip to content

Commit 9b48997

Browse files
Merge branch 'main' into MPT-15149-error-skipping-analyzing-mpt-api-client-module-is-installed-but-missing-library-stubs-or-py-typed-marker
2 parents 817d2fa + 969f8a4 commit 9b48997

30 files changed

+1330
-66
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MPT_API_BASE_URL=https://api.example.com/
2+
3+
MPT_API_TOKEN=idt:TKN-4138-9324:...
4+
MPT_API_TOKEN_CLIENT=idt:TKN...
5+
MPT_API_TOKEN_OPERATIONS=idt:TKN-4138-9324:...
6+
MPT_API_TOKEN_VENDOR=idt:TKN-8857-1729:...
7+
RP_API_KEY="pytest_Ox...
8+
RP_ENDPOINT="https://reportportal.example.com"
9+
RP_LAUNCH="dev-env"

.env.sample

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: daily-e2e.yml
2+
on:
3+
schedule:
4+
- cron: '0 8 * * *'
5+
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
timeout-minutes: 10
10+
steps:
11+
- name: "Checkout"
12+
uses: actions/checkout@v4
13+
with:
14+
ref: main
15+
16+
- name: "Build test containers"
17+
run: docker compose build e2e
18+
19+
- name: "Create environment file"
20+
run: env | grep -E '^MPT_' > .env
21+
env:
22+
RP_ENDPOINT: ${{ secrets.RP_ENDPOINT }}
23+
RP_API_KEY: ${{ secrets.RP_API_KEY }}
24+
MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }}
25+
MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }}
26+
MPT_API_TOKEN_CLIENT: ${{ secrets.MPT_API_TOKEN_CLIENT }}
27+
MPT_API_TOKEN_OPERATIONS: ${{ secrets.MPT_API_TOKEN_OPERATIONS }}
28+
MPT_API_TOKEN_VENDOR: ${{ secrets.MPT_API_TOKEN_VENDOR }}
29+
30+
- name: "Run E2E test"
31+
run: docker compose run --service-ports e2e bash -c "pytest -v -p no:randomly --no-cov --reportportal --rp-launch=$RP_LAUNCH --rp-api-key=$RP_API_KEY --rp-endpoint=$RP_ENDPOINT --junitxml=e2e-report.xml tests/e2e"
32+
env:
33+
RP_LAUNCH: github-e2e-cron-main
34+
RP_ENDPOINT: ${{ secrets.RP_ENDPOINT }}
35+
RP_API_KEY: ${{ secrets.RP_API_KEY }}
36+
37+
- name: "Stop containers"
38+
if: always()
39+
run: docker compose down

.github/workflows/pr-build-merge.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
fetch-depth: 0
2525

2626
- name: "Build test containers"
27-
run: docker compose build app_test
27+
run: docker compose build app_test e2e
2828

2929
- name: "Create environment file"
3030
run: env | grep -E '^MPT_' > .env
@@ -41,7 +41,7 @@ jobs:
4141
run: docker compose run --service-ports app_test
4242

4343
- name: "Run E2E test"
44-
run: docker compose run --service-ports app_test bash -c "pytest -v -p no:randomly --no-cov --reportportal --rp-launch=$RP_LAUNCH --rp-api-key=$RP_API_KEY --rp-endpoint=$RP_ENDPOINT --junitxml=e2e-report.xml tests/e2e"
44+
run: docker compose run --service-ports e2e bash -c "pytest -v -p no:randomly --no-cov --reportportal --rp-launch=$RP_LAUNCH --rp-api-key=$RP_API_KEY --rp-endpoint=$RP_ENDPOINT --junitxml=e2e-report.xml tests/e2e"
4545
env:
4646
RP_LAUNCH: github-e2e-test
4747
RP_ENDPOINT: ${{ secrets.RP_ENDPOINT }}

.github/workflows/release.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@ jobs:
2020
with:
2121
fetch-depth: 0
2222

23+
- name: "Build test containers"
24+
run: docker compose build e2e
25+
26+
- name: "Create environment file"
27+
run: env | grep -E '^MPT_' > .env
28+
env:
29+
RP_ENDPOINT: ${{ secrets.RP_ENDPOINT }}
30+
RP_API_KEY: ${{ secrets.RP_API_KEY }}
31+
MPT_API_BASE_URL: ${{ secrets.MPT_API_BASE_URL }}
32+
MPT_API_TOKEN: ${{ secrets.MPT_API_TOKEN }}
33+
MPT_API_TOKEN_CLIENT: ${{ secrets.MPT_API_TOKEN_CLIENT }}
34+
MPT_API_TOKEN_OPERATIONS: ${{ secrets.MPT_API_TOKEN_OPERATIONS }}
35+
MPT_API_TOKEN_VENDOR: ${{ secrets.MPT_API_TOKEN_VENDOR }}
36+
37+
- name: "Run E2E test"
38+
run: docker compose run --service-ports e2e bash -c "pytest -v -p no:randomly --no-cov --reportportal --rp-launch=$RP_LAUNCH --rp-api-key=$RP_API_KEY --rp-endpoint=$RP_ENDPOINT --junitxml=e2e-report.xml tests/e2e"
39+
env:
40+
RP_LAUNCH: github-e2e-cron-main
41+
RP_ENDPOINT: ${{ secrets.RP_ENDPOINT }}
42+
RP_API_KEY: ${{ secrets.RP_API_KEY }}
43+
2344
- name: "Set up Python"
2445
uses: actions/setup-python@v5
2546
with:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,6 @@ cython_debug/
170170

171171
# VS Code dev container
172172
.devcontainer/
173+
174+
# E2E report
175+
e2e-report.xml

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,68 @@
11
# mpt-api-python-client
2+
3+
A Python client for interacting with the MPT API.
4+
5+
## Installation
6+
7+
Install as a uv dependency:
8+
9+
```bash
10+
uv add mpt-api-client
11+
cp .env.example .env
12+
```
13+
14+
## Usage
15+
16+
```python
17+
from mpt_api_client import MPTClient
18+
19+
#client = MPTClient(api_key=os.getenv("MPT_API_KEY"), base_url=os.getenv("MPT_API_URL"))
20+
client = MPTClient() # Will get the api_key and base_url from the environment variables
21+
22+
for product in client.catalog.products.iterate():
23+
print(product.name)
24+
```
25+
26+
## Async Usage
27+
28+
```python
29+
import asyncio
30+
from mpt_api_client import AsyncMPTClient
31+
32+
async def main():
33+
# client = AsyncMPTClient(api_key=os.getenv("MPT_API_KEY"), base_url=os.getenv("MPT_API_URL"))
34+
client = AsyncMPTClient() # Will get the api_key and base_url from the environment variables
35+
async for product in client.catalog.products.iterate():
36+
print(product.name)
37+
38+
asyncio.run(main())
39+
```
40+
41+
## Development
42+
43+
Clone the repository and install dependencies:
44+
45+
```bash
46+
git clone https://github.com/albertsola/mpt-api-python-client.git
47+
cd mpt-api-python-client
48+
uv add -r requirements.txt
49+
```
50+
51+
## Testing
52+
53+
Run all validations with:
54+
55+
```bash
56+
docker compose run --rm app_test
57+
```
58+
59+
Run pytest with:
60+
61+
```bash
62+
pytest tests/unit
63+
pytest tests/e2e
64+
pytest tests/seed
65+
```
66+
## License
67+
68+
MIT

e2e_config.test.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
{
2-
"catalog.product.id": "PRD-7255-3950"
2+
"catalog.product.id": "PRD-7255-3950",
3+
"accounts.seller.id": "SEL-7310-3075",
4+
"catalog.product.parameter_group.id": "PGR-7255-3950-0001"
35
}

mpt_api_client/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
APPLICATION_JSON = "application/json"

mpt_api_client/http/mixins.py

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Self
44
from urllib.parse import urljoin
55

6+
from mpt_api_client.constants import APPLICATION_JSON
67
from mpt_api_client.http.query_state import QueryState
78
from mpt_api_client.http.types import FileTypes, Response
89
from mpt_api_client.models import Collection, FileModel, ResourceData
@@ -84,7 +85,7 @@ def create(
8485
files[data_key] = (
8586
None,
8687
_json_to_file_payload(resource_data),
87-
"application/json",
88+
APPLICATION_JSON,
8889
)
8990
response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
9091

@@ -105,6 +106,77 @@ def download(self, resource_id: str) -> FileModel:
105106
return FileModel(response)
106107

107108

109+
class CreateWithIconMixin[Model]:
110+
"""Create resource with icon mixin."""
111+
112+
def create(
113+
self,
114+
resource_data: ResourceData,
115+
icon: FileTypes,
116+
data_key: str,
117+
icon_key: str,
118+
) -> Model:
119+
"""Create resource with icon.
120+
121+
Args:
122+
resource_data: Resource data.
123+
data_key: Key for the resource data.
124+
icon: Icon image in jpg, png, GIF, etc.
125+
icon_key: Key for the icon.
126+
127+
Returns:
128+
Created resource.
129+
"""
130+
files: dict[str, FileTypes] = {}
131+
files[data_key] = (
132+
None,
133+
json.dumps(resource_data),
134+
APPLICATION_JSON,
135+
)
136+
files[icon_key] = icon
137+
response = self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
138+
139+
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
140+
141+
142+
class UpdateWithIconMixin[Model]:
143+
"""Update resource with icon mixin."""
144+
145+
def update(
146+
self,
147+
resource_id: str,
148+
resource_data: ResourceData,
149+
icon: FileTypes,
150+
data_key: str,
151+
icon_key: str,
152+
) -> Model:
153+
"""Update resource with icon.
154+
155+
Args:
156+
resource_id: Resource ID.
157+
resource_data: Resource data.
158+
data_key: Key for the resource data.
159+
icon: Icon image in jpg, png, GIF, etc.
160+
icon_key: Key for the icon.
161+
162+
Returns:
163+
Updated resource.
164+
"""
165+
files: dict[str, FileTypes] = {}
166+
files[data_key] = (
167+
None,
168+
json.dumps(resource_data),
169+
APPLICATION_JSON,
170+
)
171+
files[icon_key] = icon
172+
173+
url = urljoin(f"{self.path}/", resource_id) # type: ignore[attr-defined]
174+
175+
response = self.http_client.request("put", url, files=files) # type: ignore[attr-defined]
176+
177+
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
178+
179+
108180
class AsyncCreateMixin[Model]:
109181
"""Create resource mixin."""
110182

@@ -174,7 +246,7 @@ async def create(
174246
files[data_key] = (
175247
None,
176248
_json_to_file_payload(resource_data),
177-
"application/json",
249+
APPLICATION_JSON,
178250
)
179251

180252
response = await self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
@@ -196,6 +268,77 @@ async def download(self, resource_id: str) -> FileModel:
196268
return FileModel(response)
197269

198270

271+
class AsyncCreateWithIconMixin[Model]:
272+
"""Create resource with icon mixin."""
273+
274+
async def create(
275+
self,
276+
resource_data: ResourceData,
277+
icon: FileTypes,
278+
data_key: str,
279+
icon_key: str,
280+
) -> Model:
281+
"""Create resource with icon.
282+
283+
Args:
284+
resource_data: Resource data.
285+
data_key: Key for the resource data.
286+
icon: Icon image in jpg, png, GIF, etc.
287+
icon_key: Key for the icon.
288+
289+
Returns:
290+
Created resource.
291+
"""
292+
files: dict[str, FileTypes] = {}
293+
files[data_key] = (
294+
None,
295+
json.dumps(resource_data),
296+
APPLICATION_JSON,
297+
)
298+
files[icon_key] = icon
299+
response = await self.http_client.request("post", self.path, files=files) # type: ignore[attr-defined]
300+
301+
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
302+
303+
304+
class AsyncUpdateWithIconMixin[Model]:
305+
"""Update resource with icon mixin."""
306+
307+
async def update(
308+
self,
309+
resource_id: str,
310+
resource_data: ResourceData,
311+
icon: FileTypes,
312+
data_key: str,
313+
icon_key: str,
314+
) -> Model:
315+
"""Update resource with icon.
316+
317+
Args:
318+
resource_id: Resource ID.
319+
resource_data: Resource data.
320+
data_key: Key for the resource data.
321+
icon: Icon image in jpg, png, GIF, etc.
322+
icon_key: Key for the icon.
323+
324+
Returns:
325+
Updated resource.
326+
"""
327+
files: dict[str, FileTypes] = {}
328+
files[data_key] = (
329+
None,
330+
json.dumps(resource_data),
331+
APPLICATION_JSON,
332+
)
333+
files[icon_key] = icon
334+
335+
url = urljoin(f"{self.path}/", resource_id) # type: ignore[attr-defined]
336+
337+
response = await self.http_client.request("put", url, files=files) # type: ignore[attr-defined]
338+
339+
return self._model_class.from_response(response) # type: ignore[attr-defined, no-any-return]
340+
341+
199342
class GetMixin[Model]:
200343
"""Get resource mixin."""
201344

0 commit comments

Comments
 (0)