simpro-py-sdk 0.2.0__tar.gz → 0.3.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.
Files changed (32) hide show
  1. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/PKG-INFO +75 -8
  2. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/README.md +74 -7
  3. simpro_py_sdk-0.3.0/docs/plans/2026-10-11-rate-limiting.md +1098 -0
  4. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/pyproject.toml +1 -1
  5. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/__init__.py +1 -1
  6. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/client.py +66 -12
  7. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/exceptions.py +18 -1
  8. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/__init__.py +3 -1
  9. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/_base.py +33 -1
  10. simpro_py_sdk-0.3.0/src/simpro/resources/quotes.py +17 -0
  11. simpro_py_sdk-0.3.0/src/simpro/resources/setup_webhooks.py +20 -0
  12. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/tests/test_errors.py +28 -3
  13. simpro_py_sdk-0.3.0/tests/test_rate_limiting.py +233 -0
  14. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/tests/test_resources.py +114 -30
  15. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/uv.lock +1 -1
  16. simpro_py_sdk-0.2.0/tests/test_rate_limiting.py +0 -96
  17. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/.github/workflows/ci.yml +0 -0
  18. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/.github/workflows/publish.yml +0 -0
  19. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/.gitignore +0 -0
  20. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/.python-version +0 -0
  21. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/LICENCE.txt +0 -0
  22. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/_util.py +0 -0
  23. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/py.typed +0 -0
  24. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/ratelimit.py +0 -0
  25. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/companies.py +0 -0
  26. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/customers.py +0 -0
  27. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/jobs.py +0 -0
  28. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/leads.py +0 -0
  29. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/src/simpro/resources/sites.py +0 -0
  30. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/tests/test_client.py +0 -0
  31. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/tests/test_pagination.py +0 -0
  32. {simpro_py_sdk-0.2.0 → simpro_py_sdk-0.3.0}/tests/test_ratelimit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simpro-py-sdk
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python client for the SimPro Cloud REST API (v1.0).
5
5
  Project-URL: Homepage, https://github.com/xuanpablo/simpro-py
6
6
  Project-URL: Changelog, https://github.com/xuanpablo/simpro-py/releases
@@ -78,8 +78,8 @@ doubled-prefix 404 (`Invalid route`) cannot happen.
78
78
 
79
79
  ## Resources
80
80
 
81
- 0.1.x ships read-only access to the first-slice resources, one namespace per
82
- resource, hung off the client:
81
+ The client ships read-only access to the first-slice resources, one
82
+ namespace per resource, hung off the client:
83
83
 
84
84
  | Namespace | Endpoints |
85
85
  |---|---|
@@ -87,6 +87,9 @@ resource, hung off the client:
87
87
  | `client.customers` | `list(company_id, ...)`, `get(company_id, customer_id)`, `iter_all(company_id, **filters)` |
88
88
  | `client.sites` | `list(company_id, ...)`, `get(company_id, site_id)`, `iter_all(company_id, **filters)` |
89
89
  | `client.jobs` | `list(company_id, ...)`, `get(company_id, job_id)`, `iter_all(company_id, **filters)` |
90
+ | `client.leads` | `list(company_id, ...)`, `get(company_id, lead_id)`, `iter_all(company_id, **filters)` — list items are minimal; extra fields come via `columns="ID,LeadName,Stage"` |
91
+ | `client.quotes` | `list(company_id, ...)`, `get(company_id, quote_id)`, `iter_all(company_id, **filters)` — same `columns` projection as leads |
92
+ | `client.webhooks` | `list(company_id, ...)`, `get(company_id, webhook_id)`, `iter_all(company_id)`, `create(company_id, data)`, `update(company_id, webhook_id, data)`, `delete(company_id, webhook_id)` — webhook subscriptions (`setup/webhooks`), see below |
90
93
 
91
94
  `company_id` is `0` unless your build uses SimPro multi-company.
92
95
 
@@ -125,6 +128,68 @@ since = last_sync_run_at # aware datetime; naive values are treated as UTC
125
128
  new_customers = client.customers.list(0, modified_since=since)
126
129
  ```
127
130
 
131
+ ## Webhooks
132
+
133
+ Subscriptions are fully API-managed per build/company — register one yourself
134
+ instead of asking a tenant admin:
135
+
136
+ ```python
137
+ subscription = client.webhooks.create(
138
+ 0,
139
+ {
140
+ "Name": "My integration",
141
+ "CallbackURL": "https://example.com/webhook/abc123",
142
+ "Secret": "shared-secret",
143
+ "Events": ["lead.created", "lead.updated", "lead.deleted"],
144
+ },
145
+ )
146
+ webhook_id = subscription["ID"]
147
+ client.webhooks.update(0, webhook_id, {"Events": ["lead.created"]})
148
+ client.webhooks.delete(0, webhook_id)
149
+ ```
150
+
151
+ The delivery payload is thin — the event name and the record ids only, e.g.
152
+ `{"ID": "lead.created", "reference": {"companyID": 0, "leadID": 501}}` — so
153
+ fetch the record from the resource namespace named by the event.
154
+
155
+ Deliveries are at-least-once and can be paused after repeated failures, with
156
+ no documented replay API: keep a periodic poll beside the webhook.
157
+
158
+ ## Rate limiting
159
+
160
+ SimPro enforces **10 API calls per second, per build and per IP** — a budget
161
+ shared across every integration and UI session hitting the build — and
162
+ answers over-limit traffic with `HTTP 429` (strictly enforced since April
163
+ 2025).
164
+
165
+ The client keeps you inside that budget:
166
+
167
+ ```python
168
+ client = SimproClient(base_url, api_key) # paced at 9 req/s
169
+ client = SimproClient(base_url, api_key, rate_limit=10) # right at the ceiling
170
+ client = SimproClient(base_url, api_key, rate_limit=None) # you handle pacing
171
+ ```
172
+
173
+ - Pacing uses a sliding one-second window (default **9 requests/second**,
174
+ just under the ceiling to leave headroom for other traffic on the shared
175
+ budget) and adds no dependencies.
176
+ - The limit is per *build*, not per client: pass one shared limiter to
177
+ several clients on the same tenant so they count against a single budget.
178
+
179
+ ```python
180
+ from simpro import SlidingWindowRateLimiter
181
+
182
+ limiter = SlidingWindowRateLimiter(9)
183
+ a = SimproClient(base_url, key_a, rate_limiter=limiter)
184
+ b = SimproClient(base_url, key_b, rate_limiter=limiter)
185
+ ```
186
+
187
+ - When SimPro still answers `429` (other integrations can consume the
188
+ shared budget), the client retries automatically — up to `max_retries`
189
+ times (default 3), honouring `Retry-After` when present, otherwise
190
+ exponential backoff (1s, doubling, capped at 30s). Set `max_retries=0`
191
+ to disable retrying.
192
+
128
193
  ## Errors
129
194
 
130
195
  All failures raise a subclass of `SimproError`:
@@ -134,7 +199,7 @@ All failures raise a subclass of `SimproError`:
134
199
  | `SimproAuthError` | 401, 403 |
135
200
  | `SimproNotFound` | 404 |
136
201
  | `SimproValidationError` | 422 (`.details` holds the raw error list) |
137
- | `SimproRateLimitError` | 429 |
202
+ | `SimproRateLimitError` | 429 after retries exhausted (`.retry_after` holds the parsed header, when sent) |
138
203
  | `SimproError` | anything else, including transport failures |
139
204
 
140
205
  SimPro 422 bodies are normally `{"errors": [{"path", "message", "value"}, ...]}`
@@ -143,10 +208,12 @@ but the `errors` key may be absent — both shapes are handled, and
143
208
 
144
209
  ## Deliberate omissions
145
210
 
146
- - **No retry, no rate limiting, no caching.** SimPro allows roughly 10
147
- requests/second per key; callers doing bulk pulls should pace requests and
148
- retry 429s/transport errors themselves.
149
- - **No write endpoints** in 0.1.x — read-only first.
211
+ - **No retry on transport errors** — only `429` is retried automatically
212
+ (see [Rate limiting](#rate-limiting)); connection resets and timeouts
213
+ remain the caller's job.
214
+ - **No caching.**
215
+ - **Writes are limited to webhook subscriptions** — `create`/`update`/`delete`
216
+ on `client.webhooks`; every other namespace is read-only.
150
217
  - **No DTO layer** — responses are plain JSON dicts, so SDK releases never
151
218
  lag the API.
152
219
 
@@ -36,8 +36,8 @@ doubled-prefix 404 (`Invalid route`) cannot happen.
36
36
 
37
37
  ## Resources
38
38
 
39
- 0.1.x ships read-only access to the first-slice resources, one namespace per
40
- resource, hung off the client:
39
+ The client ships read-only access to the first-slice resources, one
40
+ namespace per resource, hung off the client:
41
41
 
42
42
  | Namespace | Endpoints |
43
43
  |---|---|
@@ -45,6 +45,9 @@ resource, hung off the client:
45
45
  | `client.customers` | `list(company_id, ...)`, `get(company_id, customer_id)`, `iter_all(company_id, **filters)` |
46
46
  | `client.sites` | `list(company_id, ...)`, `get(company_id, site_id)`, `iter_all(company_id, **filters)` |
47
47
  | `client.jobs` | `list(company_id, ...)`, `get(company_id, job_id)`, `iter_all(company_id, **filters)` |
48
+ | `client.leads` | `list(company_id, ...)`, `get(company_id, lead_id)`, `iter_all(company_id, **filters)` — list items are minimal; extra fields come via `columns="ID,LeadName,Stage"` |
49
+ | `client.quotes` | `list(company_id, ...)`, `get(company_id, quote_id)`, `iter_all(company_id, **filters)` — same `columns` projection as leads |
50
+ | `client.webhooks` | `list(company_id, ...)`, `get(company_id, webhook_id)`, `iter_all(company_id)`, `create(company_id, data)`, `update(company_id, webhook_id, data)`, `delete(company_id, webhook_id)` — webhook subscriptions (`setup/webhooks`), see below |
48
51
 
49
52
  `company_id` is `0` unless your build uses SimPro multi-company.
50
53
 
@@ -83,6 +86,68 @@ since = last_sync_run_at # aware datetime; naive values are treated as UTC
83
86
  new_customers = client.customers.list(0, modified_since=since)
84
87
  ```
85
88
 
89
+ ## Webhooks
90
+
91
+ Subscriptions are fully API-managed per build/company — register one yourself
92
+ instead of asking a tenant admin:
93
+
94
+ ```python
95
+ subscription = client.webhooks.create(
96
+ 0,
97
+ {
98
+ "Name": "My integration",
99
+ "CallbackURL": "https://example.com/webhook/abc123",
100
+ "Secret": "shared-secret",
101
+ "Events": ["lead.created", "lead.updated", "lead.deleted"],
102
+ },
103
+ )
104
+ webhook_id = subscription["ID"]
105
+ client.webhooks.update(0, webhook_id, {"Events": ["lead.created"]})
106
+ client.webhooks.delete(0, webhook_id)
107
+ ```
108
+
109
+ The delivery payload is thin — the event name and the record ids only, e.g.
110
+ `{"ID": "lead.created", "reference": {"companyID": 0, "leadID": 501}}` — so
111
+ fetch the record from the resource namespace named by the event.
112
+
113
+ Deliveries are at-least-once and can be paused after repeated failures, with
114
+ no documented replay API: keep a periodic poll beside the webhook.
115
+
116
+ ## Rate limiting
117
+
118
+ SimPro enforces **10 API calls per second, per build and per IP** — a budget
119
+ shared across every integration and UI session hitting the build — and
120
+ answers over-limit traffic with `HTTP 429` (strictly enforced since April
121
+ 2025).
122
+
123
+ The client keeps you inside that budget:
124
+
125
+ ```python
126
+ client = SimproClient(base_url, api_key) # paced at 9 req/s
127
+ client = SimproClient(base_url, api_key, rate_limit=10) # right at the ceiling
128
+ client = SimproClient(base_url, api_key, rate_limit=None) # you handle pacing
129
+ ```
130
+
131
+ - Pacing uses a sliding one-second window (default **9 requests/second**,
132
+ just under the ceiling to leave headroom for other traffic on the shared
133
+ budget) and adds no dependencies.
134
+ - The limit is per *build*, not per client: pass one shared limiter to
135
+ several clients on the same tenant so they count against a single budget.
136
+
137
+ ```python
138
+ from simpro import SlidingWindowRateLimiter
139
+
140
+ limiter = SlidingWindowRateLimiter(9)
141
+ a = SimproClient(base_url, key_a, rate_limiter=limiter)
142
+ b = SimproClient(base_url, key_b, rate_limiter=limiter)
143
+ ```
144
+
145
+ - When SimPro still answers `429` (other integrations can consume the
146
+ shared budget), the client retries automatically — up to `max_retries`
147
+ times (default 3), honouring `Retry-After` when present, otherwise
148
+ exponential backoff (1s, doubling, capped at 30s). Set `max_retries=0`
149
+ to disable retrying.
150
+
86
151
  ## Errors
87
152
 
88
153
  All failures raise a subclass of `SimproError`:
@@ -92,7 +157,7 @@ All failures raise a subclass of `SimproError`:
92
157
  | `SimproAuthError` | 401, 403 |
93
158
  | `SimproNotFound` | 404 |
94
159
  | `SimproValidationError` | 422 (`.details` holds the raw error list) |
95
- | `SimproRateLimitError` | 429 |
160
+ | `SimproRateLimitError` | 429 after retries exhausted (`.retry_after` holds the parsed header, when sent) |
96
161
  | `SimproError` | anything else, including transport failures |
97
162
 
98
163
  SimPro 422 bodies are normally `{"errors": [{"path", "message", "value"}, ...]}`
@@ -101,10 +166,12 @@ but the `errors` key may be absent — both shapes are handled, and
101
166
 
102
167
  ## Deliberate omissions
103
168
 
104
- - **No retry, no rate limiting, no caching.** SimPro allows roughly 10
105
- requests/second per key; callers doing bulk pulls should pace requests and
106
- retry 429s/transport errors themselves.
107
- - **No write endpoints** in 0.1.x — read-only first.
169
+ - **No retry on transport errors** — only `429` is retried automatically
170
+ (see [Rate limiting](#rate-limiting)); connection resets and timeouts
171
+ remain the caller's job.
172
+ - **No caching.**
173
+ - **Writes are limited to webhook subscriptions** — `create`/`update`/`delete`
174
+ on `client.webhooks`; every other namespace is read-only.
108
175
  - **No DTO layer** — responses are plain JSON dicts, so SDK releases never
109
176
  lag the API.
110
177