weclappy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weclappy-0.1.0/PKG-INFO +88 -0
- weclappy-0.1.0/README.md +73 -0
- weclappy-0.1.0/pyproject.toml +25 -0
- weclappy-0.1.0/setup.cfg +4 -0
- weclappy-0.1.0/tests/test_weclappy_integration.py +80 -0
- weclappy-0.1.0/weclappy.egg-info/PKG-INFO +88 -0
- weclappy-0.1.0/weclappy.egg-info/SOURCES.txt +9 -0
- weclappy-0.1.0/weclappy.egg-info/dependency_links.txt +1 -0
- weclappy-0.1.0/weclappy.egg-info/requires.txt +1 -0
- weclappy-0.1.0/weclappy.egg-info/top_level.txt +1 -0
- weclappy-0.1.0/weclappy.py +249 -0
weclappy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: weclappy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python client for the Weclapp API.
|
|
5
|
+
Author-email: Markus Wals <markus@wals.pro>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://wals.pro/
|
|
8
|
+
Project-URL: Repository, https://github.com/Wals-pro/weclappy
|
|
9
|
+
Project-URL: Documentation, https://github.com/Wals-pro/weclappy#readme
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.26.0
|
|
15
|
+
|
|
16
|
+
# weclappy
|
|
17
|
+
|
|
18
|
+
The weclapp Python Client.
|
|
19
|
+
|
|
20
|
+
There is no lightweight, simple weclapp client library available for Python currently. Let's build it together.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
The goal of this library is to provide a minimal, threaded client that handles pagination effectively when fetching lists from the weclapp API. It is capable of retrieving large volumes of data by parallelizing page requests, significantly reducing wait times. This library is designed to be lean with no unnecessary bloat, allowing you to get started very quickly.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Threaded Pagination:** Fetch multiple pages concurrently for enhanced performance.
|
|
29
|
+
- **Minimal Dependencies:** Only dependency is [`requests`](https://pypi.org/project/requests/).
|
|
30
|
+
- **Simplicity:** A lean bloat free solution to interact with the weclapp API.
|
|
31
|
+
- **Open Source:** Free to use in any project, with contributions and improvements highly welcome.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the package via pip:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install weclapp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
|
|
45
|
+
from weclapp import Weclapp
|
|
46
|
+
|
|
47
|
+
# Initialize the client with your base URL and API key
|
|
48
|
+
client = Weclapp("https://acme.weclapp.com/webapp/api/v1", "your_api_key")
|
|
49
|
+
|
|
50
|
+
# Fetch a single entity by ID, e.g., 'salesOrder' with ID '12345'
|
|
51
|
+
sales_order = client.get("salesOrder", id="12345")
|
|
52
|
+
|
|
53
|
+
# Fetch paginated results for an entity, e.g., 'salesOrder' with a filter
|
|
54
|
+
sales_orders = client.get_all("salesOrder", { "salesOrderPaymentType-eq": "ADVANCE_PAYMENT" }, threaded=True)
|
|
55
|
+
|
|
56
|
+
# Create a new entity, e.g., 'salesOrder'
|
|
57
|
+
new_sales_order = client.post("salesOrder", { "customerId": "12345", "commission": "Hello, world!" })
|
|
58
|
+
|
|
59
|
+
# Update an existing entity, e.g., 'salesOrder' with ID '12345', ignoreMissingProperties is True per default
|
|
60
|
+
updated_sales_order = client.put("salesOrder", id="12345", data={ "commission": "Hello, universe!" })
|
|
61
|
+
|
|
62
|
+
# Delete an entity, e.g., 'salesOrder' with ID '12345'
|
|
63
|
+
client.delete("salesOrder", id="12345")
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
Contributions are very welcome. Any improvements, bug fixes, or new features are gladly accepted. Let’s build this client together to make working with the weclapp API as efficient as possible.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
This project is licensed under the MIT License. Feel free to use it in your commercial projects with no restrictions.
|
|
75
|
+
|
|
76
|
+
# Get in touch
|
|
77
|
+
|
|
78
|
+
If you are interested in working with us or want us to implement your integrations, then book a call. You can always book a call with me at
|
|
79
|
+
https://wals.pro/termin.
|
|
80
|
+
|
|
81
|
+
## Support
|
|
82
|
+
|
|
83
|
+
Feel free to use this library in all your projects for free. If you have a lot of fun and build something great with it, consider buying me a coffee.
|
|
84
|
+
|
|
85
|
+
## Follow me on:
|
|
86
|
+
- [LinkedIn](https://www.linkedin.com/in/markuswals)
|
|
87
|
+
- [YouTube](https://www.youtube.com/@wals-pro)
|
|
88
|
+
|
weclappy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# weclappy
|
|
2
|
+
|
|
3
|
+
The weclapp Python Client.
|
|
4
|
+
|
|
5
|
+
There is no lightweight, simple weclapp client library available for Python currently. Let's build it together.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
The goal of this library is to provide a minimal, threaded client that handles pagination effectively when fetching lists from the weclapp API. It is capable of retrieving large volumes of data by parallelizing page requests, significantly reducing wait times. This library is designed to be lean with no unnecessary bloat, allowing you to get started very quickly.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Threaded Pagination:** Fetch multiple pages concurrently for enhanced performance.
|
|
14
|
+
- **Minimal Dependencies:** Only dependency is [`requests`](https://pypi.org/project/requests/).
|
|
15
|
+
- **Simplicity:** A lean bloat free solution to interact with the weclapp API.
|
|
16
|
+
- **Open Source:** Free to use in any project, with contributions and improvements highly welcome.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install the package via pip:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install weclapp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
|
|
30
|
+
from weclapp import Weclapp
|
|
31
|
+
|
|
32
|
+
# Initialize the client with your base URL and API key
|
|
33
|
+
client = Weclapp("https://acme.weclapp.com/webapp/api/v1", "your_api_key")
|
|
34
|
+
|
|
35
|
+
# Fetch a single entity by ID, e.g., 'salesOrder' with ID '12345'
|
|
36
|
+
sales_order = client.get("salesOrder", id="12345")
|
|
37
|
+
|
|
38
|
+
# Fetch paginated results for an entity, e.g., 'salesOrder' with a filter
|
|
39
|
+
sales_orders = client.get_all("salesOrder", { "salesOrderPaymentType-eq": "ADVANCE_PAYMENT" }, threaded=True)
|
|
40
|
+
|
|
41
|
+
# Create a new entity, e.g., 'salesOrder'
|
|
42
|
+
new_sales_order = client.post("salesOrder", { "customerId": "12345", "commission": "Hello, world!" })
|
|
43
|
+
|
|
44
|
+
# Update an existing entity, e.g., 'salesOrder' with ID '12345', ignoreMissingProperties is True per default
|
|
45
|
+
updated_sales_order = client.put("salesOrder", id="12345", data={ "commission": "Hello, universe!" })
|
|
46
|
+
|
|
47
|
+
# Delete an entity, e.g., 'salesOrder' with ID '12345'
|
|
48
|
+
client.delete("salesOrder", id="12345")
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Contributing
|
|
53
|
+
|
|
54
|
+
Contributions are very welcome. Any improvements, bug fixes, or new features are gladly accepted. Let’s build this client together to make working with the weclapp API as efficient as possible.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
This project is licensed under the MIT License. Feel free to use it in your commercial projects with no restrictions.
|
|
60
|
+
|
|
61
|
+
# Get in touch
|
|
62
|
+
|
|
63
|
+
If you are interested in working with us or want us to implement your integrations, then book a call. You can always book a call with me at
|
|
64
|
+
https://wals.pro/termin.
|
|
65
|
+
|
|
66
|
+
## Support
|
|
67
|
+
|
|
68
|
+
Feel free to use this library in all your projects for free. If you have a lot of fun and build something great with it, consider buying me a coffee.
|
|
69
|
+
|
|
70
|
+
## Follow me on:
|
|
71
|
+
- [LinkedIn](https://www.linkedin.com/in/markuswals)
|
|
72
|
+
- [YouTube](https://www.youtube.com/@wals-pro)
|
|
73
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "weclappy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Python client for the Weclapp API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Markus Wals", email = "markus@wals.pro" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.26.0"
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent"
|
|
21
|
+
]
|
|
22
|
+
[project.urls]
|
|
23
|
+
"Homepage" = "https://wals.pro/"
|
|
24
|
+
"Repository" = "https://github.com/Wals-pro/weclappy"
|
|
25
|
+
"Documentation" = "https://github.com/Wals-pro/weclappy#readme"
|
weclappy-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# tests/test_weclappy_integration.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
import pytest
|
|
7
|
+
from weclappy import Weclapp, WeclappAPIError
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(scope="module")
|
|
10
|
+
def client() -> Weclapp:
|
|
11
|
+
"""
|
|
12
|
+
Fixture to create a Weclapp client using environment variables.
|
|
13
|
+
Skips tests if the required environment variables are not set.
|
|
14
|
+
"""
|
|
15
|
+
base_url = os.environ.get("WECLAPP_BASE_URL")
|
|
16
|
+
api_key = os.environ.get("WECLAPP_API_KEY")
|
|
17
|
+
if not base_url or not api_key:
|
|
18
|
+
pytest.skip("Environment variables WECLAPP_BASE_URL and WECLAPP_API_KEY must be set for integration tests.")
|
|
19
|
+
return Weclapp(base_url, api_key)
|
|
20
|
+
|
|
21
|
+
def test_get_all_salesorders(client: Weclapp) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Test that get_all returns a list from the 'salesOrder' endpoint.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
results = client.get_all("salesOrder", limit=5)
|
|
27
|
+
except WeclappAPIError as e:
|
|
28
|
+
pytest.skip(f"API not accessible or no sales orders available: {e}")
|
|
29
|
+
assert isinstance(results, list)
|
|
30
|
+
|
|
31
|
+
def test_get_salesorder_by_id(client: Weclapp) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Test retrieving a single salesOrder record using a known id.
|
|
34
|
+
The salesOrder id must be provided in the environment variable WECLAPP_TEST_SALESORDER_ID.
|
|
35
|
+
"""
|
|
36
|
+
salesorder_id = os.environ.get("WECLAPP_TEST_SALESORDER_ID")
|
|
37
|
+
if not salesorder_id:
|
|
38
|
+
pytest.skip("Environment variable WECLAPP_TEST_SALESORDER_ID not set for test_get_salesorder_by_id.")
|
|
39
|
+
record = client.get("salesOrder", id=salesorder_id)
|
|
40
|
+
assert isinstance(record, dict)
|
|
41
|
+
assert record.get("id") == salesorder_id
|
|
42
|
+
|
|
43
|
+
def test_create_update_delete_salesorder(client: Weclapp) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Test creating a salesOrder record, updating it, and then deleting it.
|
|
46
|
+
This test performs write operations on the test environment.
|
|
47
|
+
"""
|
|
48
|
+
customer_id = os.environ.get("WECLAPP_TEST_CUSTOMER_ID")
|
|
49
|
+
# Create a unique order number for testing
|
|
50
|
+
unique_order_number = f"TEST-{int(time.time())}-{uuid.uuid4().hex[:6]}"
|
|
51
|
+
create_payload = {
|
|
52
|
+
"customerId": customer_id,
|
|
53
|
+
"orderNumber": unique_order_number,
|
|
54
|
+
"description": "Test Sales Order created by integration tests"
|
|
55
|
+
}
|
|
56
|
+
created = client.post("salesOrder", data=create_payload)
|
|
57
|
+
assert isinstance(created, dict)
|
|
58
|
+
assert "id" in created
|
|
59
|
+
record_id = created["id"]
|
|
60
|
+
|
|
61
|
+
# Verify creation by fetching the new record
|
|
62
|
+
created_record = client.get("salesOrder", id=record_id)
|
|
63
|
+
assert isinstance(created_record, dict)
|
|
64
|
+
assert created_record.get("orderNumber") == unique_order_number
|
|
65
|
+
|
|
66
|
+
# Update the record using the proper endpoint pattern (e.g., salesOrder/id/{id})
|
|
67
|
+
update_payload = {
|
|
68
|
+
"orderNumber": unique_order_number,
|
|
69
|
+
"description": "Updated Test Sales Order"
|
|
70
|
+
}
|
|
71
|
+
updated = client.put(f"salesOrder/id/{record_id}", data=update_payload)
|
|
72
|
+
assert isinstance(updated, dict)
|
|
73
|
+
assert updated.get("id") == record_id
|
|
74
|
+
if "description" in updated:
|
|
75
|
+
assert updated["description"] == "Updated Test Sales Order"
|
|
76
|
+
|
|
77
|
+
# Delete the record.
|
|
78
|
+
# Use dryRun mode if you do not want to permanently delete test data.
|
|
79
|
+
delete_response = client.delete("salesOrder", id=record_id, params={"dryRun": True})
|
|
80
|
+
assert isinstance(delete_response, dict)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: weclappy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python client for the Weclapp API.
|
|
5
|
+
Author-email: Markus Wals <markus@wals.pro>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://wals.pro/
|
|
8
|
+
Project-URL: Repository, https://github.com/Wals-pro/weclappy
|
|
9
|
+
Project-URL: Documentation, https://github.com/Wals-pro/weclappy#readme
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: requests>=2.26.0
|
|
15
|
+
|
|
16
|
+
# weclappy
|
|
17
|
+
|
|
18
|
+
The weclapp Python Client.
|
|
19
|
+
|
|
20
|
+
There is no lightweight, simple weclapp client library available for Python currently. Let's build it together.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
The goal of this library is to provide a minimal, threaded client that handles pagination effectively when fetching lists from the weclapp API. It is capable of retrieving large volumes of data by parallelizing page requests, significantly reducing wait times. This library is designed to be lean with no unnecessary bloat, allowing you to get started very quickly.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Threaded Pagination:** Fetch multiple pages concurrently for enhanced performance.
|
|
29
|
+
- **Minimal Dependencies:** Only dependency is [`requests`](https://pypi.org/project/requests/).
|
|
30
|
+
- **Simplicity:** A lean bloat free solution to interact with the weclapp API.
|
|
31
|
+
- **Open Source:** Free to use in any project, with contributions and improvements highly welcome.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the package via pip:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install weclapp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
|
|
45
|
+
from weclapp import Weclapp
|
|
46
|
+
|
|
47
|
+
# Initialize the client with your base URL and API key
|
|
48
|
+
client = Weclapp("https://acme.weclapp.com/webapp/api/v1", "your_api_key")
|
|
49
|
+
|
|
50
|
+
# Fetch a single entity by ID, e.g., 'salesOrder' with ID '12345'
|
|
51
|
+
sales_order = client.get("salesOrder", id="12345")
|
|
52
|
+
|
|
53
|
+
# Fetch paginated results for an entity, e.g., 'salesOrder' with a filter
|
|
54
|
+
sales_orders = client.get_all("salesOrder", { "salesOrderPaymentType-eq": "ADVANCE_PAYMENT" }, threaded=True)
|
|
55
|
+
|
|
56
|
+
# Create a new entity, e.g., 'salesOrder'
|
|
57
|
+
new_sales_order = client.post("salesOrder", { "customerId": "12345", "commission": "Hello, world!" })
|
|
58
|
+
|
|
59
|
+
# Update an existing entity, e.g., 'salesOrder' with ID '12345', ignoreMissingProperties is True per default
|
|
60
|
+
updated_sales_order = client.put("salesOrder", id="12345", data={ "commission": "Hello, universe!" })
|
|
61
|
+
|
|
62
|
+
# Delete an entity, e.g., 'salesOrder' with ID '12345'
|
|
63
|
+
client.delete("salesOrder", id="12345")
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
Contributions are very welcome. Any improvements, bug fixes, or new features are gladly accepted. Let’s build this client together to make working with the weclapp API as efficient as possible.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
This project is licensed under the MIT License. Feel free to use it in your commercial projects with no restrictions.
|
|
75
|
+
|
|
76
|
+
# Get in touch
|
|
77
|
+
|
|
78
|
+
If you are interested in working with us or want us to implement your integrations, then book a call. You can always book a call with me at
|
|
79
|
+
https://wals.pro/termin.
|
|
80
|
+
|
|
81
|
+
## Support
|
|
82
|
+
|
|
83
|
+
Feel free to use this library in all your projects for free. If you have a lot of fun and build something great with it, consider buying me a coffee.
|
|
84
|
+
|
|
85
|
+
## Follow me on:
|
|
86
|
+
- [LinkedIn](https://www.linkedin.com/in/markuswals)
|
|
87
|
+
- [YouTube](https://www.youtube.com/@wals-pro)
|
|
88
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.26.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
weclappy
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A Python client for the Weclapp API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import logging
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from requests.adapters import HTTPAdapter
|
|
13
|
+
from urllib3.util.retry import Retry
|
|
14
|
+
|
|
15
|
+
# Module-level constants
|
|
16
|
+
DEFAULT_PAGE_SIZE = 1000
|
|
17
|
+
DEFAULT_MAX_WORKERS = 10
|
|
18
|
+
|
|
19
|
+
# Module-level logger
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WeclappAPIError(Exception):
|
|
24
|
+
"""Custom exception for Weclapp API errors."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Weclapp:
|
|
29
|
+
"""
|
|
30
|
+
Client for interacting with the Weclapp API.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
base_url: str
|
|
34
|
+
session: requests.Session
|
|
35
|
+
|
|
36
|
+
def __init__(self, base_url: str, api_key: str) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Initialize the Weclapp client.
|
|
39
|
+
|
|
40
|
+
:param base_url: Base URL for the API.
|
|
41
|
+
:param api_key: Authentication token.
|
|
42
|
+
"""
|
|
43
|
+
self.base_url = base_url.rstrip('/') + '/'
|
|
44
|
+
self.session = requests.Session()
|
|
45
|
+
self.session.headers.update({
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"AuthenticationToken": api_key
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# Configure HTTP retry strategy.
|
|
51
|
+
retry_strategy = Retry(
|
|
52
|
+
total=3,
|
|
53
|
+
backoff_factor=0.3,
|
|
54
|
+
status_forcelist=[500, 502, 503, 504],
|
|
55
|
+
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"]
|
|
56
|
+
)
|
|
57
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
58
|
+
self.session.mount("http://", adapter)
|
|
59
|
+
self.session.mount("https://", adapter)
|
|
60
|
+
|
|
61
|
+
def _send_request(self, method: str, url: str, **kwargs: Any) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Send an HTTP request and return the JSON response.
|
|
64
|
+
|
|
65
|
+
If the response status code is 204 or no content is present, return an empty dict.
|
|
66
|
+
|
|
67
|
+
:param method: HTTP method.
|
|
68
|
+
:param url: URL for the request.
|
|
69
|
+
:param kwargs: Additional request parameters.
|
|
70
|
+
:return: JSON response as a dict, or {} for 204 responses.
|
|
71
|
+
:raises WeclappAPIError: on request failure.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
response = self.session.request(method, url, **kwargs)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
# Return {} if there is no content (204 No Content) or empty body
|
|
77
|
+
if response.status_code == 204 or not response.content.strip():
|
|
78
|
+
return {}
|
|
79
|
+
else:
|
|
80
|
+
return response.json()
|
|
81
|
+
except requests.exceptions.RequestException as e:
|
|
82
|
+
logger.error(f"HTTP {method} request failed for {url}: {e}")
|
|
83
|
+
raise WeclappAPIError(f"HTTP {method} request failed for {url}: {e}. Details: {response.text}") from e
|
|
84
|
+
|
|
85
|
+
def get(
|
|
86
|
+
self,
|
|
87
|
+
endpoint: str,
|
|
88
|
+
id: Optional[str] = None,
|
|
89
|
+
params: Optional[Dict[str, Any]] = None
|
|
90
|
+
) -> Union[List[Any], Dict[str, Any]]:
|
|
91
|
+
"""
|
|
92
|
+
Perform a GET request. If an id is provided, fetch a single record using the
|
|
93
|
+
URL pattern 'endpoint/id/{id}'. Otherwise, fetch records as a list from the endpoint.
|
|
94
|
+
|
|
95
|
+
:param endpoint: API endpoint.
|
|
96
|
+
:param id: Optional identifier to fetch a single record.
|
|
97
|
+
:param params: Query parameters.
|
|
98
|
+
:return: A single record as a dict if id is provided, or a list of records otherwise.
|
|
99
|
+
:raises WeclappAPIError: on request failure.
|
|
100
|
+
"""
|
|
101
|
+
if id is not None:
|
|
102
|
+
new_endpoint = f"{endpoint}/id/{id}"
|
|
103
|
+
url = urljoin(self.base_url, new_endpoint)
|
|
104
|
+
logger.debug(f"GET single record from {url} with params {params}")
|
|
105
|
+
return self._send_request("GET", url, params=params)
|
|
106
|
+
else:
|
|
107
|
+
url = urljoin(self.base_url, endpoint)
|
|
108
|
+
logger.debug(f"GET {url} with params {params}")
|
|
109
|
+
data = self._send_request("GET", url, params=params)
|
|
110
|
+
return data.get('result', [])
|
|
111
|
+
|
|
112
|
+
def get_all(
|
|
113
|
+
self,
|
|
114
|
+
entity: str,
|
|
115
|
+
params: Optional[Dict[str, Any]] = None,
|
|
116
|
+
limit: Optional[int] = None,
|
|
117
|
+
threaded: bool = False,
|
|
118
|
+
max_workers: int = DEFAULT_MAX_WORKERS
|
|
119
|
+
) -> List[Any]:
|
|
120
|
+
"""
|
|
121
|
+
Retrieve all records for the given entity with automatic pagination.
|
|
122
|
+
|
|
123
|
+
:param entity: Entity name, e.g. 'salesOrder'.
|
|
124
|
+
:param params: Query parameters.
|
|
125
|
+
:param limit: Limit total records returned.
|
|
126
|
+
:param threaded: Fetch pages in parallel if True.
|
|
127
|
+
:param max_workers: Maximum parallel threads (default is 10).
|
|
128
|
+
:return: List of records.
|
|
129
|
+
:raises WeclappAPIError: on request failure.
|
|
130
|
+
"""
|
|
131
|
+
params = params.copy() if params is not None else {}
|
|
132
|
+
results: List[Any] = []
|
|
133
|
+
|
|
134
|
+
if not threaded:
|
|
135
|
+
# Sequential pagination.
|
|
136
|
+
params['page'] = 1
|
|
137
|
+
params['pageSize'] = limit if (limit is not None and limit < DEFAULT_PAGE_SIZE) else DEFAULT_PAGE_SIZE
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
url = urljoin(self.base_url, entity)
|
|
141
|
+
logger.info(f"Fetching page {params['page']} for {entity}")
|
|
142
|
+
logger.debug(f"GET {url} with params {params}")
|
|
143
|
+
data = self._send_request("GET", url, params=params)
|
|
144
|
+
current_page = data.get('result', [])
|
|
145
|
+
results.extend(current_page)
|
|
146
|
+
|
|
147
|
+
if len(current_page) < params['pageSize'] or (limit is not None and len(results) >= limit):
|
|
148
|
+
break
|
|
149
|
+
params['page'] += 1
|
|
150
|
+
|
|
151
|
+
return results[:limit] if limit is not None else results
|
|
152
|
+
|
|
153
|
+
else:
|
|
154
|
+
# Parallel pagination.
|
|
155
|
+
count_endpoint = f"{entity}/count"
|
|
156
|
+
logger.info(f"Fetching total count for {entity} with params {params}")
|
|
157
|
+
total_count_data = self.get(count_endpoint, params=params)
|
|
158
|
+
total_count = total_count_data if isinstance(total_count_data, int) else 0
|
|
159
|
+
|
|
160
|
+
if total_count == 0:
|
|
161
|
+
logger.info(f"No records found for entity '{entity}'")
|
|
162
|
+
return results
|
|
163
|
+
|
|
164
|
+
page_size = limit if (limit is not None and limit < DEFAULT_PAGE_SIZE) else DEFAULT_PAGE_SIZE
|
|
165
|
+
total_for_pages = total_count if (limit is None or limit > total_count) else limit
|
|
166
|
+
total_pages = math.ceil(total_for_pages / page_size)
|
|
167
|
+
|
|
168
|
+
logger.info(
|
|
169
|
+
f"Total {total_count} records for {entity}, fetching up to {total_for_pages} "
|
|
170
|
+
f"records across {total_pages} pages in parallel."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def fetch_page(page_number: int) -> List[Any]:
|
|
174
|
+
# Fetch a single page.
|
|
175
|
+
page_params = params.copy()
|
|
176
|
+
page_params['page'] = page_number
|
|
177
|
+
page_params['pageSize'] = page_size
|
|
178
|
+
url = urljoin(self.base_url, entity)
|
|
179
|
+
logger.info(f"[Threaded] Fetching page {page_number} of {total_pages} for {entity}")
|
|
180
|
+
logger.debug(f"GET {url} with params {page_params}")
|
|
181
|
+
data = self._send_request("GET", url, params=page_params)
|
|
182
|
+
return data.get('result', [])
|
|
183
|
+
|
|
184
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
185
|
+
future_to_page = {executor.submit(fetch_page, page): page for page in range(1, total_pages + 1)}
|
|
186
|
+
for future in as_completed(future_to_page):
|
|
187
|
+
page_number = future_to_page[future]
|
|
188
|
+
try:
|
|
189
|
+
page_results = future.result()
|
|
190
|
+
results.extend(page_results)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f"Error fetching page {page_number} for {entity}: {e}")
|
|
193
|
+
else:
|
|
194
|
+
logger.info(f"[Threaded] Completed page {page_number}/{total_pages} for {entity}")
|
|
195
|
+
|
|
196
|
+
return results[:limit] if limit is not None else results
|
|
197
|
+
|
|
198
|
+
def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
199
|
+
"""
|
|
200
|
+
Perform a POST request to the given endpoint.
|
|
201
|
+
|
|
202
|
+
:param endpoint: API endpoint.
|
|
203
|
+
:param data: Data to post.
|
|
204
|
+
:return: JSON response.
|
|
205
|
+
:raises WeclappAPIError: on request failure.
|
|
206
|
+
"""
|
|
207
|
+
url = urljoin(self.base_url, endpoint)
|
|
208
|
+
logger.debug(f"POST {url} - Data: {data}")
|
|
209
|
+
return self._send_request("POST", url, json=data)
|
|
210
|
+
|
|
211
|
+
def put(self, endpoint: str, data: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
212
|
+
"""
|
|
213
|
+
Perform a PUT request to the given endpoint.
|
|
214
|
+
|
|
215
|
+
:param endpoint: API endpoint.
|
|
216
|
+
:param data: Data to put.
|
|
217
|
+
:param params: Query parameters.
|
|
218
|
+
:return: JSON response.
|
|
219
|
+
:raises WeclappAPIError: on request failure.
|
|
220
|
+
"""
|
|
221
|
+
params = params.copy() if params is not None else {}
|
|
222
|
+
params.setdefault("ignoreMissingProperties", True)
|
|
223
|
+
url = urljoin(self.base_url, endpoint)
|
|
224
|
+
logger.debug(f"PUT {url} - Data: {data} - Params: {params}")
|
|
225
|
+
return self._send_request("PUT", url, json=data, params=params)
|
|
226
|
+
|
|
227
|
+
def delete(
|
|
228
|
+
self,
|
|
229
|
+
endpoint: str,
|
|
230
|
+
id: str,
|
|
231
|
+
params: Optional[Dict[str, Any]] = None
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
"""
|
|
234
|
+
Perform a DELETE request to delete a record.
|
|
235
|
+
|
|
236
|
+
Since the DELETE endpoint returns a 204 No Content response, this method
|
|
237
|
+
returns an empty dict when deletion is successful.
|
|
238
|
+
|
|
239
|
+
:param endpoint: API endpoint.
|
|
240
|
+
:param id: The identifier of the record to delete.
|
|
241
|
+
:param params: Query parameters (e.g., dryRun).
|
|
242
|
+
:return: An empty dict.
|
|
243
|
+
:raises WeclappAPIError: on request failure.
|
|
244
|
+
"""
|
|
245
|
+
params = params.copy() if params is not None else {}
|
|
246
|
+
new_endpoint = f"{endpoint}/id/{id}"
|
|
247
|
+
url = urljoin(self.base_url, new_endpoint)
|
|
248
|
+
logger.debug(f"DELETE {url} with params {params}")
|
|
249
|
+
return self._send_request("DELETE", url, params=params)
|