appnigma-sdk 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.
Files changed (68) hide show
  1. appnigma_sdk-0.1.0/.gitignore +17 -0
  2. appnigma_sdk-0.1.0/LICENSE +21 -0
  3. appnigma_sdk-0.1.0/PKG-INFO +424 -0
  4. appnigma_sdk-0.1.0/README.md +390 -0
  5. appnigma_sdk-0.1.0/pyproject.toml +77 -0
  6. appnigma_sdk-0.1.0/src/appnigma/__init__.py +37 -0
  7. appnigma_sdk-0.1.0/src/appnigma/_checks.py +96 -0
  8. appnigma_sdk-0.1.0/src/appnigma/_client.py +211 -0
  9. appnigma_sdk-0.1.0/src/appnigma/_connections.py +38 -0
  10. appnigma_sdk-0.1.0/src/appnigma/_constants.py +83 -0
  11. appnigma_sdk-0.1.0/src/appnigma/_crm.py +45 -0
  12. appnigma_sdk-0.1.0/src/appnigma/_errors.py +156 -0
  13. appnigma_sdk-0.1.0/src/appnigma/_generated/__init__.py +132 -0
  14. appnigma_sdk-0.1.0/src/appnigma/_generated/api/__init__.py +5 -0
  15. appnigma_sdk-0.1.0/src/appnigma/_generated/api/default_api.py +4195 -0
  16. appnigma_sdk-0.1.0/src/appnigma/_generated/api_client.py +842 -0
  17. appnigma_sdk-0.1.0/src/appnigma/_generated/api_response.py +21 -0
  18. appnigma_sdk-0.1.0/src/appnigma/_generated/configuration.py +654 -0
  19. appnigma_sdk-0.1.0/src/appnigma/_generated/exceptions.py +218 -0
  20. appnigma_sdk-0.1.0/src/appnigma/_generated/models/__init__.py +58 -0
  21. appnigma_sdk-0.1.0/src/appnigma/_generated/models/connection_status.py +38 -0
  22. appnigma_sdk-0.1.0/src/appnigma/_generated/models/create_connection_link_input.py +114 -0
  23. appnigma_sdk-0.1.0/src/appnigma/_generated/models/create_connection_link_output.py +90 -0
  24. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_company.py +141 -0
  25. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_company_get_output.py +92 -0
  26. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_company_list_output.py +102 -0
  27. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_contact.py +173 -0
  28. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_contact_get_output.py +92 -0
  29. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_contact_list_output.py +102 -0
  30. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_custom_field.py +99 -0
  31. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_deal.py +215 -0
  32. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_deal_get_output.py +92 -0
  33. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_deal_list_output.py +102 -0
  34. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_email.py +107 -0
  35. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_get_input.py +107 -0
  36. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_labelled.py +95 -0
  37. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_list_input.py +109 -0
  38. appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_phone.py +107 -0
  39. appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_entry.py +102 -0
  40. appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_owner.py +137 -0
  41. appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_owner_one_of.py +95 -0
  42. appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_owner_one_of1.py +97 -0
  43. appnigma_sdk-0.1.0/src/appnigma/_generated/models/get_connection_output.py +93 -0
  44. appnigma_sdk-0.1.0/src/appnigma/_generated/models/get_guide_output.py +88 -0
  45. appnigma_sdk-0.1.0/src/appnigma/_generated/models/json_value.py +197 -0
  46. appnigma_sdk-0.1.0/src/appnigma/_generated/models/list_functions_output.py +95 -0
  47. appnigma_sdk-0.1.0/src/appnigma/_generated/models/platform_request_input.py +122 -0
  48. appnigma_sdk-0.1.0/src/appnigma/_generated/models/platform_request_input_body.py +139 -0
  49. appnigma_sdk-0.1.0/src/appnigma/_generated/models/platform_request_output.py +101 -0
  50. appnigma_sdk-0.1.0/src/appnigma/_generated/models/post_input.py +130 -0
  51. appnigma_sdk-0.1.0/src/appnigma/_generated/models/post_input_record_value.py +161 -0
  52. appnigma_sdk-0.1.0/src/appnigma/_generated/models/post_output.py +90 -0
  53. appnigma_sdk-0.1.0/src/appnigma/_generated/models/pull_input.py +118 -0
  54. appnigma_sdk-0.1.0/src/appnigma/_generated/models/pull_output.py +115 -0
  55. appnigma_sdk-0.1.0/src/appnigma/_generated/models/pull_output_records_inner.py +90 -0
  56. appnigma_sdk-0.1.0/src/appnigma/_generated/models/ran_as.py +98 -0
  57. appnigma_sdk-0.1.0/src/appnigma/_generated/models/run_input.py +135 -0
  58. appnigma_sdk-0.1.0/src/appnigma/_generated/models/run_output.py +103 -0
  59. appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_error.py +92 -0
  60. appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_error_error.py +103 -0
  61. appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_error_error_details.py +107 -0
  62. appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_problem.py +90 -0
  63. appnigma_sdk-0.1.0/src/appnigma/_generated/rest.py +334 -0
  64. appnigma_sdk-0.1.0/src/appnigma/_names.py +51 -0
  65. appnigma_sdk-0.1.0/src/appnigma/_retry.py +93 -0
  66. appnigma_sdk-0.1.0/src/appnigma/_transport.py +149 -0
  67. appnigma_sdk-0.1.0/src/appnigma/_version.py +5 -0
  68. appnigma_sdk-0.1.0/src/appnigma/py.typed +0 -0
@@ -0,0 +1,17 @@
1
+ node_modules/
2
+ dist/
3
+ .turbo/
4
+ *.tgz
5
+ .env
6
+ .DS_Store
7
+ temp/
8
+ *.gem
9
+ ruby/.bundle/
10
+ ruby/vendor/
11
+ ruby/tmp/
12
+ python/.venv/
13
+ python/tmp/
14
+ __pycache__/
15
+ *.egg-info/
16
+ .pytest_cache/
17
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Appnigma AI Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,424 @@
1
+ Metadata-Version: 2.5
2
+ Name: appnigma-sdk
3
+ Version: 0.1.0
4
+ Summary: The package a customer's product installs to reach its Appnigma microserver
5
+ Project-URL: Homepage, https://docs.appnigma.ai/sdk
6
+ Project-URL: Source, https://github.com/Appnigma/ap-sdks/tree/main/python
7
+ Project-URL: Issues, https://github.com/Appnigma/ap-sdks/issues
8
+ Project-URL: Changelog, https://github.com/Appnigma/ap-sdks/releases
9
+ Author-email: Appnigma <support@appnigma.ai>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: pydantic>=2.11
25
+ Requires-Dist: python-dateutil>=2.8.2
26
+ Requires-Dist: typing-extensions>=4.7.1
27
+ Requires-Dist: urllib3<3.0.0,>=2.7.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: build~=1.3; extra == 'dev'
30
+ Requires-Dist: pytest~=9.0; extra == 'dev'
31
+ Requires-Dist: ruff~=0.17.0; extra == 'dev'
32
+ Requires-Dist: twine~=7.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # appnigma-sdk
36
+
37
+ The package your product installs to reach its Appnigma microserver. It sends
38
+ records to an end customer's HubSpot, reads them back, reads contacts,
39
+ companies and deals in one shape whichever CRM they use, runs your own
40
+ functions, calls any platform's API, and connects end customers. One package
41
+ for every customer. Nothing about your account is inside it, and it holds no
42
+ secret: the key is yours to supply.
43
+
44
+ - Python 3.11 or newer. Tested on 3.11, 3.12, 3.13, 3.14 and 3.15.
45
+ - The same calls, error codes and retry rules as the TypeScript package
46
+ `@appnigma/sdk`, with names in Python style.
47
+ - Not tied to Django, Flask or any framework.
48
+
49
+ ## Install
50
+
51
+ ```sh
52
+ pip install appnigma-sdk
53
+ ```
54
+
55
+ The package is installed as `appnigma-sdk` and imported as `appnigma`.
56
+
57
+ ## The key
58
+
59
+ Every call carries your SDK key. Appnigma gives you your key when your
60
+ microserver is set up; ask Appnigma for a new one if you need to replace or
61
+ cancel it. It starts with `apn_live_` and is shown once. Keep it on your
62
+ server, in the `APPNIGMA_SDK_KEY` environment variable, never in a browser or
63
+ a repo. The client reads it from there:
64
+
65
+ ```python
66
+ from appnigma import Appnigma
67
+
68
+ client = Appnigma()
69
+ ```
70
+
71
+ If you keep the key somewhere else, pass it in; a passed key wins:
72
+
73
+ ```python
74
+ client = Appnigma(key=vault.read("appnigma-sdk-key"))
75
+ ```
76
+
77
+ A client started with no key at all, or with a key that does not start with
78
+ `apn_live_`, raises `AuthError` at once, before any call.
79
+
80
+ ## The first call: post a record
81
+
82
+ `post` creates a record or updates the one that already matches. For a
83
+ contact, the match is on `email` unless you name another property with
84
+ `unique_by`. For a company or a deal, `unique_by` is required: name a property
85
+ the platform treats as unique, and include it in `record` with a non-empty
86
+ text value. Because it is an update when the record exists, a repeated call is
87
+ safe.
88
+
89
+ ```python
90
+ result = client.post(
91
+ end_customer_id="lawfirm-123",
92
+ platform="hubspot",
93
+ object="contact",
94
+ record={"email": "jane@firm.com", "firstname": "Jane", "lastname": "Doe"},
95
+ )
96
+ print("created" if result["created"] else "updated", result["id"])
97
+ ```
98
+
99
+ `end_customer_id` is your own id for your customer, 1 to 100 characters of
100
+ letters, digits, dot, dash or underscore. `platform` is required, and `post`
101
+ and `pull` work on `hubspot` only for now. `object` is `contact`, `company` or
102
+ `deal`. `record` uses the platform's own property names.
103
+
104
+ Every argument is passed by name. Every answer is a plain `dict` with keys in
105
+ Python style (`expires_at`, `input_schema`), except content that is not ours,
106
+ which comes back exactly as it was sent: a record's `properties`, a function's
107
+ `output` and its schemas, a platform's `headers` and `body`, a CRM record's
108
+ `raw` and the names in its `custom_fields`.
109
+
110
+ ## Reading back: pull
111
+
112
+ `pull` returns one page of records and a marker for the next. `next` is `None`
113
+ on the last page.
114
+
115
+ ```python
116
+ after = None
117
+ while True:
118
+ page = client.pull(end_customer_id="lawfirm-123", platform="hubspot", object="contact", limit=100, after=after)
119
+ for contact in page["records"]:
120
+ print(contact["id"], contact["properties"]["email"])
121
+ after = page["next"]
122
+ if after is None:
123
+ break
124
+ ```
125
+
126
+ `limit` is 1 to 100 and defaults to 100.
127
+
128
+ ## Connecting an end customer
129
+
130
+ Before calls can work for an end customer, they connect their account on that
131
+ platform once. Any platform switched on for your company can be connected, and
132
+ the end customer approves your own app there. Your product asks for a link,
133
+ shows it to the end customer, and they approve on the platform.
134
+
135
+ 1. Ask for a link. It works once and expires after 15 minutes. Opening it does
136
+ not use it up: it is used when the end customer presses Connect, so a mail
137
+ scanner or a link preview opening it first does no harm.
138
+ 2. Send the end customer to the link's `url`. They log in to the platform and
139
+ approve.
140
+ 3. When they are done, they land on your `return_url` with `status=connected`,
141
+ or with `status=failed` and a `reason`. `return_url` must be HTTPS and at
142
+ most 2048 bytes when written as a JSON string, about 2046 plain characters.
143
+ 4. Confirm with `connections.get`. The connection is written before the end
144
+ customer lands, so the answer is already there.
145
+
146
+ ```python
147
+ link = client.connections.create_link(
148
+ end_customer_id="lawfirm-123",
149
+ platform="hubspot",
150
+ return_url="https://app.example.com/integrations/hubspot/done",
151
+ )
152
+ print(f"send the end customer to {link['url']} before {link['expires_at']}")
153
+
154
+ connection = client.connections.get(end_customer_id="lawfirm-123", platform="hubspot")
155
+ if connection["status"] == "connected":
156
+ print("ready to post and pull")
157
+ ```
158
+
159
+ `status` is `connected`, `needs_reconnect` or `disconnected`. An end customer
160
+ who is already connected can open a new link to connect again, for example
161
+ after changing HubSpot accounts. An end customer the API has never seen is a
162
+ `NotFoundError`, not a status.
163
+
164
+ The id travels in the URL for this call, so an id that is only `.` or `..` is
165
+ refused before the call with `InputError`: a URL drops those, and the request
166
+ would reach the wrong route. Do not use them as end customer ids.
167
+
168
+ ## CRM data in one shape: crm
169
+
170
+ `crm.contacts`, `crm.companies` and `crm.deals` read an end customer's CRM in
171
+ one shape, whichever CRM they use. Each has `list` and `get`, and each takes
172
+ `end_customer_id` and `platform`.
173
+
174
+ ```python
175
+ page = client.crm.contacts.list(end_customer_id="lawfirm-123", platform="attio", limit=50)
176
+ for contact in page["records"]:
177
+ email = contact["emails"][0]["value"] if contact["emails"] else None
178
+ print(contact["full_name"], email)
179
+
180
+ # The next page, while there is one.
181
+ if page["next"]:
182
+ client.crm.contacts.list(end_customer_id="lawfirm-123", platform="attio", cursor=page["next"])
183
+
184
+ deal = client.crm.deals.get(end_customer_id="lawfirm-123", platform="hubspot", id="1234")["record"]
185
+ print(deal["name"], deal["amount"], deal["currency"], deal["status"])
186
+ ```
187
+
188
+ Every field is always there: a value the CRM does not have is `None`, never
189
+ left out.
190
+
191
+ | Object | Fields |
192
+ | --- | --- |
193
+ | Every record | `id`, `platform`, `created_at`, `updated_at`, `custom_fields`, and `raw`, the record exactly as the CRM sent it |
194
+ | Contact | `first_name`, `last_name`, `full_name`, `emails`, `phones` (the primary one first), `company_id`, `owner_id` |
195
+ | Company | `name`, `domain`, `owner_id` |
196
+ | Deal | `name`, `amount`, `currency`, `status` (`open`, `won` or `lost`), `stage`, `pipeline`, `probability`, `close_date`, `closed_at`, `company_id`, `owner_id` |
197
+
198
+ `custom_fields` is keyed by the CRM's own field names, and `raw` is exactly
199
+ what the CRM sent. A platform with no unified data yet for that object is a
200
+ `NotAvailableError` with `DATA_TYPE_NOT_AVAILABLE`; reach it with `request`
201
+ instead. An id that does not exist is a `NotFoundError` with
202
+ `RECORD_NOT_FOUND`. Reads are retried like `pull`.
203
+
204
+ ## Anything else on the platform: request
205
+
206
+ `request` calls the end customer's platform's own API, for anything the shared
207
+ shapes do not cover. It works on every platform switched on for you, including
208
+ ones with no unified data yet.
209
+
210
+ ```python
211
+ answer = client.request(
212
+ end_customer_id="lawfirm-123",
213
+ platform="hubspot",
214
+ method="GET",
215
+ path="/crm/v3/objects/notes?limit=10",
216
+ )
217
+ if answer["status"] == 200:
218
+ print(answer["body"])
219
+ ```
220
+
221
+ `path` is a path on the platform's own API, with its query: never a full
222
+ address, and it cannot step out of that API, so `..` and `//host` are refused.
223
+ `body` is sent as JSON; leave it out on a `GET`.
224
+
225
+ The answer is the platform's own: its `status`, its `headers` (content type,
226
+ paging and rate limits) and its `body`, parsed from JSON when the platform
227
+ answered JSON and as text otherwise. A refusal from the platform comes back
228
+ the same way, with its status, rather than as an error: check
229
+ `answer["status"]`. A `GET` is retried like `pull`; any other method is made
230
+ once, because only you know whether repeating it is harmless.
231
+
232
+ ## Your own functions: catalog
233
+
234
+ Beyond `post` and `pull`, your microserver can hold functions written for you.
235
+ `catalog` lists them, with the platform each one is for and the JSON Schema of
236
+ its input and output. Leave `platform` out for every platform,
237
+ or name one to narrow the list.
238
+
239
+ ```python
240
+ for function in client.catalog(platform="hubspot")["functions"]:
241
+ print(function["platform"], function["name"], function["description"])
242
+ ```
243
+
244
+ A function's `owner` is `{"type": "customer"}` when every end customer can use
245
+ it, or `{"type": "end_customer", "end_customer_id": "..."}` when it was
246
+ written for one of them. `input_schema` and `output_schema` are JSON Schema,
247
+ as written. Each function has one live version: a change to it replaces it
248
+ when the change goes live. Until a function has gone live for you, the list is
249
+ empty, not an error.
250
+
251
+ ## Your own functions: run
252
+
253
+ `run` calls one of them, by platform and name, for one end customer. The input
254
+ is checked against the function's shape before anything runs, so a wrong or
255
+ missing field is an `InputError` listing every problem. Formats and patterns,
256
+ such as an email or a fixed layout for a code, are checked by the function
257
+ itself, so those refusals arrive from it with the same code. The answer is
258
+ `{"output": ..., "ran_as": ...}`: the output in the shape the function
259
+ declared, with its keys as the function wrote them, and whose function ran.
260
+ `ran_as["owner"]` is `"end_customer"` when the end customer has a function of
261
+ their own by that name and it ran, `"customer"` when yours did;
262
+ `ran_as["version"]` is the version that ran.
263
+
264
+ ```python
265
+ result = client.run(
266
+ end_customer_id="lawfirm-123",
267
+ platform="hubspot",
268
+ function_name="invoice-to-deal",
269
+ input={"invoiceId": "inv_1042", "amount": 1250},
270
+ )
271
+ print("deal", result["output"]["dealId"], "ran as", result["ran_as"]["owner"], result["ran_as"]["version"])
272
+ ```
273
+
274
+ `platform` and `function_name` are both required: the same name can exist on
275
+ two platforms and mean two different things. What runs is always the
276
+ function's live version. A name that exists only on another platform is a
277
+ `NotFoundError` whose `details["platforms"]` says which. A function that was
278
+ live and was removed from your logic is retired: calling it is a
279
+ `RetiredError` with `FUNCTION_RETIRED`, whose `details` name the function and
280
+ when it was retired. A platform that is not switched on for you is an
281
+ `AuthError` with `PLATFORM_NOT_ENABLED`.
282
+
283
+ `run` is made once and never retried: it is your own function, and only you
284
+ know whether running it twice is harmless. If it fails with a network or
285
+ server error, decide in your code whether to call again.
286
+
287
+ ## Your own guide
288
+
289
+ One page, written for your company: install, the key, connecting an end
290
+ customer to each platform switched on for you, `post`, `pull` and `run`, every
291
+ one of your functions with its fields and an example call, and every error
292
+ with what to do about it. `guide` answers it as markdown, built from what is
293
+ live right now.
294
+
295
+ ```python
296
+ from pathlib import Path
297
+
298
+ Path("appnigma-guide.md").write_text(client.guide()["guide"])
299
+ ```
300
+
301
+ ## Telling failures apart
302
+
303
+ Every failure is an exception you can catch by class. All of them inherit from
304
+ `AppnigmaError`, so one `except AppnigmaError` catches anything from
305
+ Appnigma. Every error carries `code`, `message`, `status` and `request_id`;
306
+ `str(error)` is the message. Quote the request id when you write to
307
+ support@appnigma.ai, and never include your key or a token.
308
+
309
+ | Class | When | Extra field |
310
+ | --- | --- | --- |
311
+ | `InputError` | 400. A field is wrong or missing. Also raised locally for a bad setting or a bad end customer id, before any call. | `problems`, a list of `{"field": ..., "problem": ...}` |
312
+ | `AuthError` | 401 or 403. The key is missing, unknown or cancelled, the microserver is not enabled, or the platform is not switched on for you. Also raised at start with no key. | |
313
+ | `NotFoundError` | 404. No connection for that end customer on that platform, no such function, or no such record (`RECORD_NOT_FOUND`). When the function exists on another platform, `details["platforms"]` names it. | |
314
+ | `ReconnectError` | 409 with `CONNECTION_NEEDS_RECONNECT`. The end customer has to approve again. | |
315
+ | `NotAvailableError` | Any other 409. The platform has no unified data for that yet (`DATA_TYPE_NOT_AVAILABLE`); use `request`. | |
316
+ | `RetiredError` | 410 with `FUNCTION_RETIRED`. The function was live and was removed from your logic; it will not answer again. | |
317
+ | `RateLimitError` | 429, after the retries. Too many calls, or the platform asked us to slow down. | `retry_after_seconds`, or `None` when the server named no wait |
318
+ | `ServerError` | Any 5xx, after the retries. Also any successful answer whose body is not JSON. | |
319
+ | `NetworkError` | The API could not be reached, or an attempt ran out of time, after the retries. `status` and `request_id` are `None`. | `__cause__`, the underlying exception |
320
+
321
+ ```python
322
+ from appnigma import (
323
+ AuthError,
324
+ InputError,
325
+ NetworkError,
326
+ NotFoundError,
327
+ RateLimitError,
328
+ ReconnectError,
329
+ ServerError,
330
+ )
331
+
332
+ try:
333
+ client.post(end_customer_id="lawfirm-123", platform="hubspot", object="contact", record={})
334
+ except InputError as error:
335
+ for problem in error.problems:
336
+ print(problem["field"], problem["problem"])
337
+ except AuthError as error:
338
+ print("check the key:", error.code)
339
+ except NotFoundError:
340
+ print("this end customer is not connected yet")
341
+ except ReconnectError:
342
+ print("ask the end customer to connect again")
343
+ except RateLimitError as error:
344
+ print(f"wait {error.retry_after_seconds} seconds")
345
+ except ServerError as error:
346
+ print("our side failed, quote this id to support:", error.request_id)
347
+ except NetworkError as error:
348
+ print("could not reach the API:", error.__cause__)
349
+ ```
350
+
351
+ Or catch the base class:
352
+
353
+ ```python
354
+ from appnigma import AppnigmaError
355
+
356
+ try:
357
+ client.connections.get(end_customer_id="nobody-here", platform="hubspot")
358
+ except AppnigmaError as error:
359
+ print(type(error).__name__, error.code, error.status, error.request_id)
360
+ ```
361
+
362
+ `code` is one of the strings in `ERROR_CODES`, or whatever a newer server
363
+ sent. A code this version of the package does not know is kept as is, never
364
+ replaced.
365
+
366
+ ## Retries and time limits
367
+
368
+ A call is tried again only when trying again is safe: the API could not be
369
+ reached, it answered 5xx, or it answered 429. A 400, 401, 403, 404, 409 or 410
370
+ is never retried, because the same call would be refused the same way. By default
371
+ a call is made up to three times.
372
+
373
+ `connections.create_link`, `run`, and `request` with a method other than `GET`
374
+ are the exceptions: each is made once, never retried. Every call to
375
+ `create_link` makes a new link, and a repeat after a lost answer would leave a
376
+ working link nobody saw; `run` is your own function, and whether a repeat is
377
+ harmless is yours to know. If either fails, decide in your code whether to
378
+ call again.
379
+
380
+ The wait between tries is what the `Retry-After` header says when the server
381
+ sent one, otherwise half a second and then a second, with a little random
382
+ spread so many clients do not retry in the same instant. A `Retry-After`
383
+ longer than `max_retry_wait_ms` is not waited for: the call fails at once with
384
+ a `RateLimitError` that carries the wait, so your code decides what to do with
385
+ half a minute rather than sitting through it.
386
+
387
+ Every attempt has a time limit, 30 seconds by default. An attempt that runs out
388
+ of time counts as a network failure and is retried like one. Each call can set
389
+ its own limit:
390
+
391
+ ```python
392
+ page = client.pull(end_customer_id="lawfirm-123", platform="hubspot", object="contact", limit=1, timeout_ms=10_000)
393
+ print("one record:", len(page["records"]))
394
+ ```
395
+
396
+ Times are in milliseconds, as in the TypeScript and Ruby packages, so the
397
+ numbers are the same in all three.
398
+
399
+ ## Settings
400
+
401
+ All passed to `Appnigma()`, and all optional. `key` can be passed first
402
+ without its name; the rest are passed by name.
403
+
404
+ | Setting | Default | Meaning |
405
+ | --- | --- | --- |
406
+ | `key` | the `APPNIGMA_SDK_KEY` environment variable | Your SDK key. |
407
+ | `timeout_ms` | `30_000` | How long one attempt may take. A whole number of milliseconds, at most 2147483647. |
408
+ | `max_retries` | `2` | How many times a call is tried again. `0` means one attempt. |
409
+ | `max_retry_wait_ms` | `30_000` | The longest `Retry-After` the client will sit through, in milliseconds, at most 2147483647. |
410
+
411
+ Per call, by name: `timeout_ms`.
412
+
413
+ ## What is exported
414
+
415
+ From `appnigma`: `Appnigma`, `AppnigmaError` and its nine subclasses, the
416
+ constants `ERROR_CODES`, `OBJECTS`, `CONNECTION_STATUSES` and
417
+ `PLATFORM_REQUEST_METHODS`, `__version__`, and `API_DESCRIPTION_VERSION`, the
418
+ version of the published description of the API that this version of the
419
+ package was generated from. Names that start with an underscore are not part
420
+ of the package's interface and can change in any release.
421
+
422
+ ## License
423
+
424
+ MIT. See `LICENSE`.