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.
@@ -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,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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ weclappy.py
4
+ tests/test_weclappy_integration.py
5
+ weclappy.egg-info/PKG-INFO
6
+ weclappy.egg-info/SOURCES.txt
7
+ weclappy.egg-info/dependency_links.txt
8
+ weclappy.egg-info/requires.txt
9
+ weclappy.egg-info/top_level.txt
@@ -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)