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.
- appnigma_sdk-0.1.0/.gitignore +17 -0
- appnigma_sdk-0.1.0/LICENSE +21 -0
- appnigma_sdk-0.1.0/PKG-INFO +424 -0
- appnigma_sdk-0.1.0/README.md +390 -0
- appnigma_sdk-0.1.0/pyproject.toml +77 -0
- appnigma_sdk-0.1.0/src/appnigma/__init__.py +37 -0
- appnigma_sdk-0.1.0/src/appnigma/_checks.py +96 -0
- appnigma_sdk-0.1.0/src/appnigma/_client.py +211 -0
- appnigma_sdk-0.1.0/src/appnigma/_connections.py +38 -0
- appnigma_sdk-0.1.0/src/appnigma/_constants.py +83 -0
- appnigma_sdk-0.1.0/src/appnigma/_crm.py +45 -0
- appnigma_sdk-0.1.0/src/appnigma/_errors.py +156 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/__init__.py +132 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/api/__init__.py +5 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/api/default_api.py +4195 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/api_client.py +842 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/api_response.py +21 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/configuration.py +654 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/exceptions.py +218 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/__init__.py +58 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/connection_status.py +38 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/create_connection_link_input.py +114 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/create_connection_link_output.py +90 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_company.py +141 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_company_get_output.py +92 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_company_list_output.py +102 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_contact.py +173 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_contact_get_output.py +92 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_contact_list_output.py +102 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_custom_field.py +99 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_deal.py +215 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_deal_get_output.py +92 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_deal_list_output.py +102 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_email.py +107 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_get_input.py +107 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_labelled.py +95 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_list_input.py +109 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/crm_phone.py +107 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_entry.py +102 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_owner.py +137 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_owner_one_of.py +95 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/function_owner_one_of1.py +97 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/get_connection_output.py +93 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/get_guide_output.py +88 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/json_value.py +197 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/list_functions_output.py +95 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/platform_request_input.py +122 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/platform_request_input_body.py +139 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/platform_request_output.py +101 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/post_input.py +130 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/post_input_record_value.py +161 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/post_output.py +90 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/pull_input.py +118 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/pull_output.py +115 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/pull_output_records_inner.py +90 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/ran_as.py +98 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/run_input.py +135 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/run_output.py +103 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_error.py +92 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_error_error.py +103 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_error_error_details.py +107 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/models/sdk_problem.py +90 -0
- appnigma_sdk-0.1.0/src/appnigma/_generated/rest.py +334 -0
- appnigma_sdk-0.1.0/src/appnigma/_names.py +51 -0
- appnigma_sdk-0.1.0/src/appnigma/_retry.py +93 -0
- appnigma_sdk-0.1.0/src/appnigma/_transport.py +149 -0
- appnigma_sdk-0.1.0/src/appnigma/_version.py +5 -0
- appnigma_sdk-0.1.0/src/appnigma/py.typed +0 -0
|
@@ -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`.
|