talki 0.1.0b1__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 (209) hide show
  1. talki-0.1.0b1/LICENSE +21 -0
  2. talki-0.1.0b1/PKG-INFO +307 -0
  3. talki-0.1.0b1/README.md +279 -0
  4. talki-0.1.0b1/pyproject.toml +83 -0
  5. talki-0.1.0b1/pyproject.toml.orig +71 -0
  6. talki-0.1.0b1/talki/__init__.py +36 -0
  7. talki-0.1.0b1/talki/_talki.py +192 -0
  8. talki-0.1.0b1/talki/_transport.py +297 -0
  9. talki-0.1.0b1/talki/api/__init__.py +1 -0
  10. talki-0.1.0b1/talki/api/account/__init__.py +1 -0
  11. talki-0.1.0b1/talki/api/account/complete_onboarding.py +240 -0
  12. talki-0.1.0b1/talki/api/account/get_account.py +253 -0
  13. talki-0.1.0b1/talki/api/account/get_forwarding_check.py +253 -0
  14. talki-0.1.0b1/talki/api/account/start_forwarding_check.py +300 -0
  15. talki-0.1.0b1/talki/api/account/update_account.py +337 -0
  16. talki-0.1.0b1/talki/api/appointments/__init__.py +1 -0
  17. talki-0.1.0b1/talki/api/appointments/cancel_appointment.py +259 -0
  18. talki-0.1.0b1/talki/api/appointments/create_appointment.py +275 -0
  19. talki-0.1.0b1/talki/api/appointments/get_appointment.py +236 -0
  20. talki-0.1.0b1/talki/api/appointments/list_appointments.py +400 -0
  21. talki-0.1.0b1/talki/api/appointments/reschedule_appointment.py +299 -0
  22. talki-0.1.0b1/talki/api/appointments/restore_appointment.py +277 -0
  23. talki-0.1.0b1/talki/api/availability/__init__.py +1 -0
  24. talki-0.1.0b1/talki/api/availability/get_availability.py +344 -0
  25. talki-0.1.0b1/talki/api/caller_rules/__init__.py +1 -0
  26. talki-0.1.0b1/talki/api/caller_rules/create_caller_rule.py +313 -0
  27. talki-0.1.0b1/talki/api/caller_rules/delete_caller_rule.py +251 -0
  28. talki-0.1.0b1/talki/api/caller_rules/list_blocked_call_attempts.py +292 -0
  29. talki-0.1.0b1/talki/api/caller_rules/list_caller_rules.py +315 -0
  30. talki-0.1.0b1/talki/api/caller_rules/update_caller_rule.py +275 -0
  31. talki-0.1.0b1/talki/api/calls/__init__.py +1 -0
  32. talki-0.1.0b1/talki/api/calls/count_calls.py +277 -0
  33. talki-0.1.0b1/talki/api/calls/count_refused_calls.py +249 -0
  34. talki-0.1.0b1/talki/api/calls/delete_call.py +267 -0
  35. talki-0.1.0b1/talki/api/calls/export_call.py +271 -0
  36. talki-0.1.0b1/talki/api/calls/export_calls.py +403 -0
  37. talki-0.1.0b1/talki/api/calls/get_call.py +234 -0
  38. talki-0.1.0b1/talki/api/calls/get_call_stats.py +330 -0
  39. talki-0.1.0b1/talki/api/calls/get_call_transcript.py +240 -0
  40. talki-0.1.0b1/talki/api/calls/get_refused_call_summary.py +330 -0
  41. talki-0.1.0b1/talki/api/calls/list_calls.py +539 -0
  42. talki-0.1.0b1/talki/api/calls/list_refused_calls.py +288 -0
  43. talki-0.1.0b1/talki/api/calls/mark_all_calls_read.py +270 -0
  44. talki-0.1.0b1/talki/api/calls/update_call.py +273 -0
  45. talki-0.1.0b1/talki/api/closures/__init__.py +1 -0
  46. talki-0.1.0b1/talki/api/closures/create_closure.py +291 -0
  47. talki-0.1.0b1/talki/api/closures/delete_closure.py +255 -0
  48. talki-0.1.0b1/talki/api/closures/list_closures.py +334 -0
  49. talki-0.1.0b1/talki/api/closures/update_closure.py +287 -0
  50. talki-0.1.0b1/talki/api/contacts/__init__.py +1 -0
  51. talki-0.1.0b1/talki/api/contacts/delete_contact.py +259 -0
  52. talki-0.1.0b1/talki/api/contacts/get_contact.py +236 -0
  53. talki-0.1.0b1/talki/api/contacts/list_contacts.py +267 -0
  54. talki-0.1.0b1/talki/api/contacts/upsert_contact.py +270 -0
  55. talki-0.1.0b1/talki/api/locations/__init__.py +1 -0
  56. talki-0.1.0b1/talki/api/locations/list_locations.py +227 -0
  57. talki-0.1.0b1/talki/api/pending_contacts/__init__.py +1 -0
  58. talki-0.1.0b1/talki/api/pending_contacts/approve_pending_contact.py +284 -0
  59. talki-0.1.0b1/talki/api/pending_contacts/discard_pending_contact.py +251 -0
  60. talki-0.1.0b1/talki/api/pending_contacts/list_pending_contacts.py +219 -0
  61. talki-0.1.0b1/talki/api/resources/__init__.py +1 -0
  62. talki-0.1.0b1/talki/api/resources/create_resource.py +316 -0
  63. talki-0.1.0b1/talki/api/resources/delete_resource.py +271 -0
  64. talki-0.1.0b1/talki/api/resources/get_resource.py +240 -0
  65. talki-0.1.0b1/talki/api/resources/list_resources.py +316 -0
  66. talki-0.1.0b1/talki/api/resources/update_resource.py +283 -0
  67. talki-0.1.0b1/talki/api/sandbox/__init__.py +1 -0
  68. talki-0.1.0b1/talki/api/sandbox/create_sandbox_call.py +263 -0
  69. talki-0.1.0b1/talki/api/sandbox/list_sandbox_scenarios.py +229 -0
  70. talki-0.1.0b1/talki/api/sandbox/reset_sandbox.py +245 -0
  71. talki-0.1.0b1/talki/api/services/__init__.py +1 -0
  72. talki-0.1.0b1/talki/api/services/create_service.py +328 -0
  73. talki-0.1.0b1/talki/api/services/delete_service.py +271 -0
  74. talki-0.1.0b1/talki/api/services/get_service.py +236 -0
  75. talki-0.1.0b1/talki/api/services/list_services.py +340 -0
  76. talki-0.1.0b1/talki/api/services/update_service.py +287 -0
  77. talki-0.1.0b1/talki/api/tags/__init__.py +1 -0
  78. talki-0.1.0b1/talki/api/tags/create_tag.py +258 -0
  79. talki-0.1.0b1/talki/api/tags/delete_tag.py +251 -0
  80. talki-0.1.0b1/talki/api/tags/list_tags.py +215 -0
  81. talki-0.1.0b1/talki/api/tags/tag_contact.py +267 -0
  82. talki-0.1.0b1/talki/api/tags/untag_contact.py +267 -0
  83. talki-0.1.0b1/talki/api/tags/update_tag.py +271 -0
  84. talki-0.1.0b1/talki/api/unresolved_requests/__init__.py +1 -0
  85. talki-0.1.0b1/talki/api/unresolved_requests/get_unresolved_request.py +236 -0
  86. talki-0.1.0b1/talki/api/unresolved_requests/list_unresolved_requests.py +343 -0
  87. talki-0.1.0b1/talki/api/unresolved_requests/teach_unresolved_request.py +324 -0
  88. talki-0.1.0b1/talki/api/unresolved_requests/update_unresolved_request.py +283 -0
  89. talki-0.1.0b1/talki/api/usage/__init__.py +1 -0
  90. talki-0.1.0b1/talki/api/usage/get_usage.py +217 -0
  91. talki-0.1.0b1/talki/api/usage/get_usage_history.py +268 -0
  92. talki-0.1.0b1/talki/api/voices/__init__.py +1 -0
  93. talki-0.1.0b1/talki/api/voices/get_voice_preview.py +234 -0
  94. talki-0.1.0b1/talki/api/voices/list_voices.py +219 -0
  95. talki-0.1.0b1/talki/api/working_hours/__init__.py +1 -0
  96. talki-0.1.0b1/talki/api/working_hours/get_working_hours.py +253 -0
  97. talki-0.1.0b1/talki/api/working_hours/replace_working_hours.py +321 -0
  98. talki-0.1.0b1/talki/client.py +268 -0
  99. talki-0.1.0b1/talki/errors.py +16 -0
  100. talki-0.1.0b1/talki/exceptions.py +200 -0
  101. talki-0.1.0b1/talki/models/__init__.py +211 -0
  102. talki-0.1.0b1/talki/models/account.py +384 -0
  103. talki-0.1.0b1/talki/models/account_languages.py +72 -0
  104. talki-0.1.0b1/talki/models/account_line.py +123 -0
  105. talki-0.1.0b1/talki/models/account_update.py +448 -0
  106. talki-0.1.0b1/talki/models/answers_as.py +34 -0
  107. talki-0.1.0b1/talki/models/appointment.py +191 -0
  108. talki-0.1.0b1/talki/models/appointment_client.py +102 -0
  109. talki-0.1.0b1/talki/models/appointment_create.py +195 -0
  110. talki-0.1.0b1/talki/models/appointment_list.py +102 -0
  111. talki-0.1.0b1/talki/models/appointment_reschedule.py +178 -0
  112. talki-0.1.0b1/talki/models/appointment_status.py +34 -0
  113. talki-0.1.0b1/talki/models/availability.py +119 -0
  114. talki-0.1.0b1/talki/models/availability_day.py +95 -0
  115. talki-0.1.0b1/talki/models/availability_interval.py +73 -0
  116. talki-0.1.0b1/talki/models/blocked_call_attempt.py +107 -0
  117. talki-0.1.0b1/talki/models/blocked_call_attempt_list.py +99 -0
  118. talki-0.1.0b1/talki/models/call.py +383 -0
  119. talki-0.1.0b1/talki/models/call_appointment_change.py +70 -0
  120. talki-0.1.0b1/talki/models/call_caller.py +100 -0
  121. talki-0.1.0b1/talki/models/call_count.py +62 -0
  122. talki-0.1.0b1/talki/models/call_list.py +103 -0
  123. talki-0.1.0b1/talki/models/call_outcome.py +38 -0
  124. talki-0.1.0b1/talki/models/call_stats.py +197 -0
  125. talki-0.1.0b1/talki/models/call_stats_caller.py +87 -0
  126. talki-0.1.0b1/talki/models/call_stats_day.py +78 -0
  127. talki-0.1.0b1/talki/models/call_transcript.py +128 -0
  128. talki-0.1.0b1/talki/models/call_transfer.py +77 -0
  129. talki-0.1.0b1/talki/models/call_update.py +96 -0
  130. talki-0.1.0b1/talki/models/caller_rule.py +120 -0
  131. talki-0.1.0b1/talki/models/caller_rule_create.py +103 -0
  132. talki-0.1.0b1/talki/models/caller_rule_kind.py +34 -0
  133. talki-0.1.0b1/talki/models/caller_rule_list.py +99 -0
  134. talki-0.1.0b1/talki/models/caller_rule_update.py +105 -0
  135. talki-0.1.0b1/talki/models/closure.py +168 -0
  136. talki-0.1.0b1/talki/models/closure_create.py +229 -0
  137. talki-0.1.0b1/talki/models/closure_kind.py +34 -0
  138. talki-0.1.0b1/talki/models/closure_list.py +100 -0
  139. talki-0.1.0b1/talki/models/closure_update.py +250 -0
  140. talki-0.1.0b1/talki/models/contact.py +187 -0
  141. talki-0.1.0b1/talki/models/contact_list.py +101 -0
  142. talki-0.1.0b1/talki/models/contact_upsert.py +170 -0
  143. talki-0.1.0b1/talki/models/error_detail.py +106 -0
  144. talki-0.1.0b1/talki/models/error_detail_type.py +38 -0
  145. talki-0.1.0b1/talki/models/error_envelope.py +68 -0
  146. talki-0.1.0b1/talki/models/export_format.py +34 -0
  147. talki-0.1.0b1/talki/models/fake_call_request.py +74 -0
  148. talki-0.1.0b1/talki/models/fake_call_response.py +126 -0
  149. talki-0.1.0b1/talki/models/forwarding_check.py +145 -0
  150. talki-0.1.0b1/talki/models/forwarding_check_status.py +36 -0
  151. talki-0.1.0b1/talki/models/inbox_status.py +36 -0
  152. talki-0.1.0b1/talki/models/line_status.py +35 -0
  153. talki-0.1.0b1/talki/models/list_closures_kind.py +35 -0
  154. talki-0.1.0b1/talki/models/location.py +166 -0
  155. talki-0.1.0b1/talki/models/location_line.py +124 -0
  156. talki-0.1.0b1/talki/models/location_list.py +100 -0
  157. talki-0.1.0b1/talki/models/location_status.py +34 -0
  158. talki-0.1.0b1/talki/models/mark_all_read_result.py +62 -0
  159. talki-0.1.0b1/talki/models/pending_contact.py +122 -0
  160. talki-0.1.0b1/talki/models/pending_contact_approval.py +115 -0
  161. talki-0.1.0b1/talki/models/pending_contact_list.py +99 -0
  162. talki-0.1.0b1/talki/models/range_unit.py +34 -0
  163. talki-0.1.0b1/talki/models/refused_call.py +140 -0
  164. talki-0.1.0b1/talki/models/refused_call_list.py +99 -0
  165. talki-0.1.0b1/talki/models/refused_call_summary.py +126 -0
  166. talki-0.1.0b1/talki/models/refused_call_summary_by_reason.py +47 -0
  167. talki-0.1.0b1/talki/models/reset_response.py +94 -0
  168. talki-0.1.0b1/talki/models/resource.py +212 -0
  169. talki-0.1.0b1/talki/models/resource_create.py +106 -0
  170. talki-0.1.0b1/talki/models/resource_list.py +100 -0
  171. talki-0.1.0b1/talki/models/resource_mode.py +34 -0
  172. talki-0.1.0b1/talki/models/resource_service_link.py +71 -0
  173. talki-0.1.0b1/talki/models/resource_unit_class.py +112 -0
  174. talki-0.1.0b1/talki/models/resource_update.py +95 -0
  175. talki-0.1.0b1/talki/models/scenario_summary.py +87 -0
  176. talki-0.1.0b1/talki/models/service.py +300 -0
  177. talki-0.1.0b1/talki/models/service_booking_mode.py +34 -0
  178. talki-0.1.0b1/talki/models/service_create.py +172 -0
  179. talki-0.1.0b1/talki/models/service_list.py +100 -0
  180. talki-0.1.0b1/talki/models/service_ref.py +86 -0
  181. talki-0.1.0b1/talki/models/service_update.py +159 -0
  182. talki-0.1.0b1/talki/models/tag.py +96 -0
  183. talki-0.1.0b1/talki/models/tag_create.py +70 -0
  184. talki-0.1.0b1/talki/models/tag_list.py +99 -0
  185. talki-0.1.0b1/talki/models/tag_update.py +94 -0
  186. talki-0.1.0b1/talki/models/transcript_utterance.py +132 -0
  187. talki-0.1.0b1/talki/models/transcript_utterance_speaker.py +35 -0
  188. talki-0.1.0b1/talki/models/transfer_number.py +71 -0
  189. talki-0.1.0b1/talki/models/transfer_number_input.py +72 -0
  190. talki-0.1.0b1/talki/models/unresolved_request.py +173 -0
  191. talki-0.1.0b1/talki/models/unresolved_request_list.py +100 -0
  192. talki-0.1.0b1/talki/models/unresolved_request_status.py +35 -0
  193. talki-0.1.0b1/talki/models/unresolved_request_teach.py +99 -0
  194. talki-0.1.0b1/talki/models/unresolved_request_teaching.py +86 -0
  195. talki-0.1.0b1/talki/models/unresolved_request_update.py +108 -0
  196. talki-0.1.0b1/talki/models/usage.py +165 -0
  197. talki-0.1.0b1/talki/models/usage_history.py +89 -0
  198. talki-0.1.0b1/talki/models/usage_month.py +96 -0
  199. talki-0.1.0b1/talki/models/voice.py +89 -0
  200. talki-0.1.0b1/talki/models/voice_gender.py +34 -0
  201. talki-0.1.0b1/talki/models/voice_list.py +99 -0
  202. talki-0.1.0b1/talki/models/working_hours.py +89 -0
  203. talki-0.1.0b1/talki/models/working_hours_slot.py +79 -0
  204. talki-0.1.0b1/talki/models/working_hours_update.py +78 -0
  205. talki-0.1.0b1/talki/oauth.py +625 -0
  206. talki-0.1.0b1/talki/pagination.py +131 -0
  207. talki-0.1.0b1/talki/py.typed +1 -0
  208. talki-0.1.0b1/talki/types.py +54 -0
  209. talki-0.1.0b1/talki/webhooks.py +145 -0
talki-0.1.0b1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HubTec S.r.l.
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.
talki-0.1.0b1/PKG-INFO ADDED
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.4
2
+ Name: talki
3
+ Version: 0.1.0b1
4
+ Summary: Python client for the Talki API
5
+ Keywords: talki,api,client,voice agent
6
+ Author: HubTec S.r.l.
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: httpx>=0.23.1,<0.29.0
20
+ Requires-Dist: attrs>=22.2.0
21
+ Requires-Dist: anyio>=3.0.0
22
+ Requires-Python: >=3.11
23
+ Project-URL: Homepage, https://developers.talki.it
24
+ Project-URL: Documentation, https://developers.talki.it
25
+ Project-URL: Repository, https://github.com/HubtecSrl/talki-python
26
+ Project-URL: Changelog, https://github.com/HubtecSrl/talki-python/blob/main/CHANGELOG.md
27
+ Description-Content-Type: text/markdown
28
+
29
+ # talki
30
+
31
+ The Python client for the [Talki API](https://developers.talki.it) (`/v1`):
32
+ typed, sync and async, generated from the published OpenAPI document, with a
33
+ hand-written layer for errors, retries, paging, webhooks and OAuth.
34
+
35
+ ```sh
36
+ pip install --pre talki # or: uv add --prerelease=allow talki
37
+ ```
38
+
39
+ Python 3.11 or later. Depends on httpx, attrs and anyio.
40
+
41
+ > **Beta.** The API contract is versioned by date (`Talki-Version`) and does
42
+ > not break within a version; this package's own interface may still change
43
+ > before 1.0. See the [changelog](https://github.com/HubtecSrl/talki-python/blob/main/CHANGELOG.md).
44
+
45
+ ## Call the API
46
+
47
+ ```python
48
+ from talki import Talki, unwrap
49
+ from talki.api.account import get_account
50
+ from talki.api.appointments import create_appointment
51
+
52
+ with Talki(api_key="talki_test_…") as talki: # or access_token="talki_at_…"
53
+ account = unwrap(get_account.sync_detailed(client=talki.client))
54
+ print(account.business_name)
55
+
56
+ appointment = unwrap(create_appointment.sync_detailed(client=talki.client, body=...))
57
+ ```
58
+
59
+ - Every operation is a module under `talki.api.<tag>` (its operation ID in snake
60
+ case) with four functions: `sync`, `sync_detailed`, `asyncio` and
61
+ `asyncio_detailed`. Each takes `client=talki.client`. Open `Talki` with
62
+ `with` for the sync ones and `async with` for the async ones, so its
63
+ connections are closed.
64
+ - `unwrap(response)` takes a `*_detailed` answer and returns its model, or None
65
+ for `204 No Content`; it raises for anything else (below). Its return type is
66
+ the operation's model.
67
+ - The `*_detailed` answer is a `talki.types.Response` with `status_code`,
68
+ `headers` (`ETag`, `Idempotency-Replayed`, the rate-limit headers,
69
+ `X-Request-Id`), `content` and `parsed`.
70
+ - `sync` and `asyncio` return the parsed body alone: the model, an
71
+ `ErrorEnvelope` for a declared error status, or `None` for a status the
72
+ operation does not declare, for an error that is not JSON, and for a `204`.
73
+ Prefer `unwrap` over reading these apart.
74
+ - `talki.types.UNSET` is a field left out, `None` an explicit `null`. In an
75
+ update body (`AccountUpdate`, `ServiceUpdate`, …) UNSET leaves the field as
76
+ it is and `None` sends `null`, which clears a field that allows it.
77
+
78
+ ## Errors
79
+
80
+ `unwrap` raises a subclass of `talki.ApiError` (itself a `talki.TalkiError`)
81
+ chosen by the envelope's `type`: `InvalidRequestError`, `AuthenticationError`,
82
+ `PermissionDeniedError`, `ConflictError`, `RateLimitError` (with
83
+ `retry_after`), `ServerError`. An error with no envelope, such as a proxy's answer,
84
+ is classed by its HTTP status with `code` None (a maintenance page is a 503), and a `type` this client does
85
+ not know raises `ApiError` itself. Each carries `status`, `type`, `code`,
86
+ `message`, `request_id`, `headers` and `body`. Branch on `code`; quote
87
+ `request_id` to Talki.
88
+
89
+ ```python
90
+ from talki import ConflictError
91
+
92
+ try:
93
+ unwrap(create_appointment.sync_detailed(client=talki.client, body=booking))
94
+ except ConflictError as error:
95
+ if error.code == "slot_unavailable":
96
+ ...
97
+ ```
98
+
99
+ ## Retries
100
+
101
+ `Talki` retries, `max_retries` times (default 2):
102
+
103
+ - any request that never left (connection refused, connect or pool timeout);
104
+ - any request answered 429 or `409 request_in_progress`, which the API turns
105
+ away before running it;
106
+ - a read answered 500, 502, 503 or 504, or whose connection broke after sending;
107
+ - a write with an `Idempotency-Key` answered 502, 503 or 504 by a gateway (not
108
+ the API's error envelope).
109
+
110
+ Never retried: a write the API itself answered with a 5xx, or whose connection
111
+ broke after sending (a read timeout among them), since either may come after
112
+ the write was saved. Its key keeps a repeat from applying it twice: the API
113
+ answers the key of a write that saved and then failed with that same 500 for
114
+ 24 hours, and releases a key only when nothing was saved. Read the resource to
115
+ learn which happened before writing again.
116
+
117
+ It waits as `Retry-After` asks, up to 60 seconds; an answer asking for longer
118
+ is not retried but returned, as `RateLimitError` (with `retry_after`) for a 429.
119
+ Without `Retry-After` it backs off exponentially with jitter. `timeout` applies
120
+ to each attempt. `max_retries=0` turns retries off.
121
+
122
+ ## Enums
123
+
124
+ A set of values whose description says "New values may be added" is a plain
125
+ `str` here; handle a value you do not know. Every other set is a `StrEnum`, and
126
+ gains a value only with a new dated version. Even so, a value an enum does not
127
+ list still parses: it is an instance of the enum, equal to its string, with a
128
+ name like a listed member's, so a response is never refused for it.
129
+
130
+ - To tell a listed value from another, use `value in list(SomeEnum)`, never
131
+ `value in SomeEnum`: for an unlisted value the latter differs between Python
132
+ versions.
133
+ - Enums in requests are not checked here either: an unlisted value is sent, and
134
+ the API answers `400 invalid_parameter`.
135
+
136
+ ## Version and idempotency
137
+
138
+ - `Talki` sends `Talki-Version: talki.API_VERSION`, the contract this package
139
+ was generated from. `Talki(version="…")` pins another, and `version=None`
140
+ sends none, so the version pinned on the key or app applies. Each operation
141
+ also takes `talki_version=` for one request.
142
+ - `Talki` gives every write without an `Idempotency-Key` a new one, so its own
143
+ retries are never applied twice. To retry a write yourself with the same key,
144
+ pass your own with `idempotency_key=` (`talki.new_idempotency_key()` makes
145
+ one); after an httpx error the key sent is
146
+ also in `error.request.headers["Idempotency-Key"]`. `409 request_in_progress`
147
+ means the first request with that key is still running and its outcome is
148
+ unknown: retry later with the same key. `auto_idempotency=False` sends none
149
+ unless given. Operations that require a key take it as optional here:
150
+ without `Talki`, omitting it is refused with `400 idempotency_key_required`.
151
+ - `Talki(proxy="http://…")` sends every request through a proxy; the proxy
152
+ environment variables are not read.
153
+
154
+ ## Paging and feeds
155
+
156
+ Operations that take `cursor=` return `data`, `has_more` and `next_cursor`.
157
+ `talki.pagination` walks them: pass the operation's `sync_detailed` (or
158
+ `asyncio_detailed` to `apages`) and its arguments.
159
+
160
+ ```python
161
+ from talki.pagination import pages, position, rows
162
+ from talki.api.calls import list_calls
163
+ from talki.api.appointments import list_appointments
164
+
165
+ for call in rows(pages(list_calls.sync_detailed, client=talki.client, limit=100)):
166
+ ...
167
+
168
+ for page in pages(list_appointments.sync_detailed, client=talki.client, cursor=stored):
169
+ save(page.data)
170
+ stored = position(page, stored) # store it with the rows
171
+ ```
172
+
173
+ `pages` stops when `has_more` is false, raises the `ApiError` of a page that
174
+ fails after yielding those before it, and keeps each page's row type. A page
175
+ that says `has_more` but gives no new cursor raises `PaginationError`.
176
+ `position` is for change feeds: a plain list's last page has no cursor though
177
+ it holds rows.
178
+
179
+ The change feeds (appointments, contacts, services, resources, unresolved
180
+ requests, and calls with `sync=True`) return `next_cursor` on every non-empty
181
+ page, the last included: store every non-null one and resume from it. Keep the
182
+ filters the same for a whole walk. A cursor used with another `location_id` is
183
+ refused; any other changed filter is accepted and skips the rows the first
184
+ walk filtered out.
185
+
186
+ ## Webhooks
187
+
188
+ A webhook endpoint and an app's `events_url` receive events signed in
189
+ `X-Talki-Signature`. Check it on the raw body, before parsing:
190
+
191
+ ```python
192
+ from talki.webhooks import SignatureVerificationError, construct_event, signature_of
193
+
194
+ try:
195
+ event = construct_event(raw_body, signature_of(headers), secret)
196
+ except SignatureVerificationError:
197
+ ... # answer 400 and ignore it
198
+ ```
199
+
200
+ - `construct_event` returns the envelope as a dict; `verify_signature` only
201
+ checks. Both take the body as the bytes received (a `str` is refused), and
202
+ refuse a signature more than 300 seconds from now.
203
+ - `signature_of(headers)` finds the header whatever its case.
204
+ - While a secret is rotated with an overlap, either secret verifies; pass a list
205
+ when you hold more than one.
206
+ - Deliveries are not ordered and may repeat: deduplicate on the event's `id`,
207
+ and order changes to one object by its `updated_at`. An account that also
208
+ has its own webhook endpoint at the same URL sends the same change twice,
209
+ with different `id`s.
210
+
211
+ ## OAuth apps
212
+
213
+ `talki.oauth` covers an app installed on Talki accounts: the consent URL with
214
+ PKCE, the callback, the code exchange, refresh and revocation, and a pair that
215
+ renews itself.
216
+
217
+ ```python
218
+ import secrets
219
+ from talki import Talki
220
+ from talki.oauth import OAuthApp, Pkce, authorization_url, read_callback
221
+
222
+ app = OAuthApp(client_id, client_secret) # base_url=…, for another deployment
223
+ pkce, state = Pkce.new(), secrets.token_urlsafe(16)
224
+ url = authorization_url(client_id, redirect_uri, ["calls:read"], state=state, pkce=pkce)
225
+ # … send the customer's browser to url; then, on the callback:
226
+ code = read_callback(callback_url, state=state)
227
+ tokens = app.exchange_code(code, redirect_uri=redirect_uri, code_verifier=pkce.verifier)
228
+ save(tokens)
229
+
230
+ talki = Talki(tokens=tokens, oauth_app=app, on_token_refresh=save)
231
+ ```
232
+
233
+ - With `oauth_app`, `Talki` renews the pair a minute before it expires, and
234
+ when the API answers `401` with `error="invalid_token"`, then sends the
235
+ request again. `talki.tokens` is the pair in use (a new one waiting to be
236
+ stored is not in use yet). `oauth_app` must talk to the same `base_url` as
237
+ `Talki`.
238
+ - **The refresh token rotates on every renewal, and presenting a rotated one
239
+ again revokes the installation.** One `Talki` serialises its renewals across
240
+ threads, event loops and sync and async calls. Keep one `Talki` per
241
+ installation per process, and never renew one pair from two of them.
242
+ - A renewal whose outcome is unknown (its answer lost or timed out, or a
243
+ gateway's 502, 503 or 504) may have rotated the pair. The server takes the
244
+ same refresh token once more within 60 seconds of that rotation, so `Talki`
245
+ presents it again in the same request, 10 seconds after the outcome was
246
+ lost (so as not to overlap the first), while that is still inside the 60
247
+ seconds. When it is not (a `timeout` above 45 seconds), the request raises and
248
+ the next one presents it again, which works only if the first never reached
249
+ the server: otherwise the customer has to reconnect.
250
+ - Never call the same `Talki` from `on_token_refresh`, nor synchronously from
251
+ async code that uses it: either would wait on its own renewal, so it raises
252
+ `RuntimeError` instead.
253
+ - `on_token_refresh` is called with the new pair before it is used: store it
254
+ there. If it raises, the pair stays pending in memory and every request
255
+ raises until a later call stores it; the replaced refresh token is never
256
+ presented again. A process that ends with a pair unstored can renew once more
257
+ with the old refresh token within 60 seconds; after that the customer has to
258
+ reconnect.
259
+ - Once the API refuses the refresh token (`invalid_grant`: the customer
260
+ disconnected, the pair was revoked, or Talki disabled the app) or the app's
261
+ credentials
262
+ (`invalid_client`), every request raises that `OAuthError` without asking
263
+ again. Build a new `Talki` once fixed.
264
+ - A code works once: never retry `exchange_code` with the same code.
265
+ - The token endpoints answer in RFC 6749's shape, raised as `OAuthError`
266
+ (`error`, `description`, `status`).
267
+ - Against another deployment, pass its addresses: `OAuthApp(base_url=…)`,
268
+ `Talki(base_url=…)`, `authorization_url(authorize_url=…)` and
269
+ `read_callback(issuer=…)`.
270
+
271
+ ## Not covered yet
272
+
273
+ The partner routes (`/oauth/app/*`, `/oauth/managed-accounts`,
274
+ `/oauth/enrollments`) and typed webhook event payloads are not in the OpenAPI
275
+ document yet, so not in this package. See the
276
+ [API reference](https://developers.talki.it) for them.
277
+
278
+ ## Versions
279
+
280
+ This package follows semantic versioning of its own. `talki.API_VERSION` is
281
+ the dated API version it was generated from and sends by default; a release
282
+ that moves to a newer API version says so in the changelog.
283
+
284
+ ## Development
285
+
286
+ Most of `talki/` is generated, never edited by hand: `scripts/generate.py`
287
+ runs openapi-python-client (pinned in `pyproject.toml`) over
288
+ `openapi/talki-v1.yaml` with the templates in `codegen/templates/`, and keeps
289
+ the hand-written modules listed in the script (`_talki.py`, `_transport.py`,
290
+ `exceptions.py`, `oauth.py`, `pagination.py`, `webhooks.py`).
291
+
292
+ ```sh
293
+ uv sync
294
+ uv run scripts/pull_spec.py # fetch the current OpenAPI document
295
+ uv run scripts/generate.py # regenerate talki/ from it
296
+ uv run scripts/generate.py --check # CI: fail if talki/ is stale
297
+ uv run pytest
298
+ uv run mypy
299
+ uv run ruff check . && uv run ruff format --check .
300
+ ```
301
+
302
+ To change generated code, change a template or `codegen/config.yaml`; a
303
+ change to the API itself belongs in the API.
304
+
305
+ ## License
306
+
307
+ [MIT](https://github.com/HubtecSrl/talki-python/blob/main/LICENSE)
@@ -0,0 +1,279 @@
1
+ # talki
2
+
3
+ The Python client for the [Talki API](https://developers.talki.it) (`/v1`):
4
+ typed, sync and async, generated from the published OpenAPI document, with a
5
+ hand-written layer for errors, retries, paging, webhooks and OAuth.
6
+
7
+ ```sh
8
+ pip install --pre talki # or: uv add --prerelease=allow talki
9
+ ```
10
+
11
+ Python 3.11 or later. Depends on httpx, attrs and anyio.
12
+
13
+ > **Beta.** The API contract is versioned by date (`Talki-Version`) and does
14
+ > not break within a version; this package's own interface may still change
15
+ > before 1.0. See the [changelog](https://github.com/HubtecSrl/talki-python/blob/main/CHANGELOG.md).
16
+
17
+ ## Call the API
18
+
19
+ ```python
20
+ from talki import Talki, unwrap
21
+ from talki.api.account import get_account
22
+ from talki.api.appointments import create_appointment
23
+
24
+ with Talki(api_key="talki_test_…") as talki: # or access_token="talki_at_…"
25
+ account = unwrap(get_account.sync_detailed(client=talki.client))
26
+ print(account.business_name)
27
+
28
+ appointment = unwrap(create_appointment.sync_detailed(client=talki.client, body=...))
29
+ ```
30
+
31
+ - Every operation is a module under `talki.api.<tag>` (its operation ID in snake
32
+ case) with four functions: `sync`, `sync_detailed`, `asyncio` and
33
+ `asyncio_detailed`. Each takes `client=talki.client`. Open `Talki` with
34
+ `with` for the sync ones and `async with` for the async ones, so its
35
+ connections are closed.
36
+ - `unwrap(response)` takes a `*_detailed` answer and returns its model, or None
37
+ for `204 No Content`; it raises for anything else (below). Its return type is
38
+ the operation's model.
39
+ - The `*_detailed` answer is a `talki.types.Response` with `status_code`,
40
+ `headers` (`ETag`, `Idempotency-Replayed`, the rate-limit headers,
41
+ `X-Request-Id`), `content` and `parsed`.
42
+ - `sync` and `asyncio` return the parsed body alone: the model, an
43
+ `ErrorEnvelope` for a declared error status, or `None` for a status the
44
+ operation does not declare, for an error that is not JSON, and for a `204`.
45
+ Prefer `unwrap` over reading these apart.
46
+ - `talki.types.UNSET` is a field left out, `None` an explicit `null`. In an
47
+ update body (`AccountUpdate`, `ServiceUpdate`, …) UNSET leaves the field as
48
+ it is and `None` sends `null`, which clears a field that allows it.
49
+
50
+ ## Errors
51
+
52
+ `unwrap` raises a subclass of `talki.ApiError` (itself a `talki.TalkiError`)
53
+ chosen by the envelope's `type`: `InvalidRequestError`, `AuthenticationError`,
54
+ `PermissionDeniedError`, `ConflictError`, `RateLimitError` (with
55
+ `retry_after`), `ServerError`. An error with no envelope, such as a proxy's answer,
56
+ is classed by its HTTP status with `code` None (a maintenance page is a 503), and a `type` this client does
57
+ not know raises `ApiError` itself. Each carries `status`, `type`, `code`,
58
+ `message`, `request_id`, `headers` and `body`. Branch on `code`; quote
59
+ `request_id` to Talki.
60
+
61
+ ```python
62
+ from talki import ConflictError
63
+
64
+ try:
65
+ unwrap(create_appointment.sync_detailed(client=talki.client, body=booking))
66
+ except ConflictError as error:
67
+ if error.code == "slot_unavailable":
68
+ ...
69
+ ```
70
+
71
+ ## Retries
72
+
73
+ `Talki` retries, `max_retries` times (default 2):
74
+
75
+ - any request that never left (connection refused, connect or pool timeout);
76
+ - any request answered 429 or `409 request_in_progress`, which the API turns
77
+ away before running it;
78
+ - a read answered 500, 502, 503 or 504, or whose connection broke after sending;
79
+ - a write with an `Idempotency-Key` answered 502, 503 or 504 by a gateway (not
80
+ the API's error envelope).
81
+
82
+ Never retried: a write the API itself answered with a 5xx, or whose connection
83
+ broke after sending (a read timeout among them), since either may come after
84
+ the write was saved. Its key keeps a repeat from applying it twice: the API
85
+ answers the key of a write that saved and then failed with that same 500 for
86
+ 24 hours, and releases a key only when nothing was saved. Read the resource to
87
+ learn which happened before writing again.
88
+
89
+ It waits as `Retry-After` asks, up to 60 seconds; an answer asking for longer
90
+ is not retried but returned, as `RateLimitError` (with `retry_after`) for a 429.
91
+ Without `Retry-After` it backs off exponentially with jitter. `timeout` applies
92
+ to each attempt. `max_retries=0` turns retries off.
93
+
94
+ ## Enums
95
+
96
+ A set of values whose description says "New values may be added" is a plain
97
+ `str` here; handle a value you do not know. Every other set is a `StrEnum`, and
98
+ gains a value only with a new dated version. Even so, a value an enum does not
99
+ list still parses: it is an instance of the enum, equal to its string, with a
100
+ name like a listed member's, so a response is never refused for it.
101
+
102
+ - To tell a listed value from another, use `value in list(SomeEnum)`, never
103
+ `value in SomeEnum`: for an unlisted value the latter differs between Python
104
+ versions.
105
+ - Enums in requests are not checked here either: an unlisted value is sent, and
106
+ the API answers `400 invalid_parameter`.
107
+
108
+ ## Version and idempotency
109
+
110
+ - `Talki` sends `Talki-Version: talki.API_VERSION`, the contract this package
111
+ was generated from. `Talki(version="…")` pins another, and `version=None`
112
+ sends none, so the version pinned on the key or app applies. Each operation
113
+ also takes `talki_version=` for one request.
114
+ - `Talki` gives every write without an `Idempotency-Key` a new one, so its own
115
+ retries are never applied twice. To retry a write yourself with the same key,
116
+ pass your own with `idempotency_key=` (`talki.new_idempotency_key()` makes
117
+ one); after an httpx error the key sent is
118
+ also in `error.request.headers["Idempotency-Key"]`. `409 request_in_progress`
119
+ means the first request with that key is still running and its outcome is
120
+ unknown: retry later with the same key. `auto_idempotency=False` sends none
121
+ unless given. Operations that require a key take it as optional here:
122
+ without `Talki`, omitting it is refused with `400 idempotency_key_required`.
123
+ - `Talki(proxy="http://…")` sends every request through a proxy; the proxy
124
+ environment variables are not read.
125
+
126
+ ## Paging and feeds
127
+
128
+ Operations that take `cursor=` return `data`, `has_more` and `next_cursor`.
129
+ `talki.pagination` walks them: pass the operation's `sync_detailed` (or
130
+ `asyncio_detailed` to `apages`) and its arguments.
131
+
132
+ ```python
133
+ from talki.pagination import pages, position, rows
134
+ from talki.api.calls import list_calls
135
+ from talki.api.appointments import list_appointments
136
+
137
+ for call in rows(pages(list_calls.sync_detailed, client=talki.client, limit=100)):
138
+ ...
139
+
140
+ for page in pages(list_appointments.sync_detailed, client=talki.client, cursor=stored):
141
+ save(page.data)
142
+ stored = position(page, stored) # store it with the rows
143
+ ```
144
+
145
+ `pages` stops when `has_more` is false, raises the `ApiError` of a page that
146
+ fails after yielding those before it, and keeps each page's row type. A page
147
+ that says `has_more` but gives no new cursor raises `PaginationError`.
148
+ `position` is for change feeds: a plain list's last page has no cursor though
149
+ it holds rows.
150
+
151
+ The change feeds (appointments, contacts, services, resources, unresolved
152
+ requests, and calls with `sync=True`) return `next_cursor` on every non-empty
153
+ page, the last included: store every non-null one and resume from it. Keep the
154
+ filters the same for a whole walk. A cursor used with another `location_id` is
155
+ refused; any other changed filter is accepted and skips the rows the first
156
+ walk filtered out.
157
+
158
+ ## Webhooks
159
+
160
+ A webhook endpoint and an app's `events_url` receive events signed in
161
+ `X-Talki-Signature`. Check it on the raw body, before parsing:
162
+
163
+ ```python
164
+ from talki.webhooks import SignatureVerificationError, construct_event, signature_of
165
+
166
+ try:
167
+ event = construct_event(raw_body, signature_of(headers), secret)
168
+ except SignatureVerificationError:
169
+ ... # answer 400 and ignore it
170
+ ```
171
+
172
+ - `construct_event` returns the envelope as a dict; `verify_signature` only
173
+ checks. Both take the body as the bytes received (a `str` is refused), and
174
+ refuse a signature more than 300 seconds from now.
175
+ - `signature_of(headers)` finds the header whatever its case.
176
+ - While a secret is rotated with an overlap, either secret verifies; pass a list
177
+ when you hold more than one.
178
+ - Deliveries are not ordered and may repeat: deduplicate on the event's `id`,
179
+ and order changes to one object by its `updated_at`. An account that also
180
+ has its own webhook endpoint at the same URL sends the same change twice,
181
+ with different `id`s.
182
+
183
+ ## OAuth apps
184
+
185
+ `talki.oauth` covers an app installed on Talki accounts: the consent URL with
186
+ PKCE, the callback, the code exchange, refresh and revocation, and a pair that
187
+ renews itself.
188
+
189
+ ```python
190
+ import secrets
191
+ from talki import Talki
192
+ from talki.oauth import OAuthApp, Pkce, authorization_url, read_callback
193
+
194
+ app = OAuthApp(client_id, client_secret) # base_url=…, for another deployment
195
+ pkce, state = Pkce.new(), secrets.token_urlsafe(16)
196
+ url = authorization_url(client_id, redirect_uri, ["calls:read"], state=state, pkce=pkce)
197
+ # … send the customer's browser to url; then, on the callback:
198
+ code = read_callback(callback_url, state=state)
199
+ tokens = app.exchange_code(code, redirect_uri=redirect_uri, code_verifier=pkce.verifier)
200
+ save(tokens)
201
+
202
+ talki = Talki(tokens=tokens, oauth_app=app, on_token_refresh=save)
203
+ ```
204
+
205
+ - With `oauth_app`, `Talki` renews the pair a minute before it expires, and
206
+ when the API answers `401` with `error="invalid_token"`, then sends the
207
+ request again. `talki.tokens` is the pair in use (a new one waiting to be
208
+ stored is not in use yet). `oauth_app` must talk to the same `base_url` as
209
+ `Talki`.
210
+ - **The refresh token rotates on every renewal, and presenting a rotated one
211
+ again revokes the installation.** One `Talki` serialises its renewals across
212
+ threads, event loops and sync and async calls. Keep one `Talki` per
213
+ installation per process, and never renew one pair from two of them.
214
+ - A renewal whose outcome is unknown (its answer lost or timed out, or a
215
+ gateway's 502, 503 or 504) may have rotated the pair. The server takes the
216
+ same refresh token once more within 60 seconds of that rotation, so `Talki`
217
+ presents it again in the same request, 10 seconds after the outcome was
218
+ lost (so as not to overlap the first), while that is still inside the 60
219
+ seconds. When it is not (a `timeout` above 45 seconds), the request raises and
220
+ the next one presents it again, which works only if the first never reached
221
+ the server: otherwise the customer has to reconnect.
222
+ - Never call the same `Talki` from `on_token_refresh`, nor synchronously from
223
+ async code that uses it: either would wait on its own renewal, so it raises
224
+ `RuntimeError` instead.
225
+ - `on_token_refresh` is called with the new pair before it is used: store it
226
+ there. If it raises, the pair stays pending in memory and every request
227
+ raises until a later call stores it; the replaced refresh token is never
228
+ presented again. A process that ends with a pair unstored can renew once more
229
+ with the old refresh token within 60 seconds; after that the customer has to
230
+ reconnect.
231
+ - Once the API refuses the refresh token (`invalid_grant`: the customer
232
+ disconnected, the pair was revoked, or Talki disabled the app) or the app's
233
+ credentials
234
+ (`invalid_client`), every request raises that `OAuthError` without asking
235
+ again. Build a new `Talki` once fixed.
236
+ - A code works once: never retry `exchange_code` with the same code.
237
+ - The token endpoints answer in RFC 6749's shape, raised as `OAuthError`
238
+ (`error`, `description`, `status`).
239
+ - Against another deployment, pass its addresses: `OAuthApp(base_url=…)`,
240
+ `Talki(base_url=…)`, `authorization_url(authorize_url=…)` and
241
+ `read_callback(issuer=…)`.
242
+
243
+ ## Not covered yet
244
+
245
+ The partner routes (`/oauth/app/*`, `/oauth/managed-accounts`,
246
+ `/oauth/enrollments`) and typed webhook event payloads are not in the OpenAPI
247
+ document yet, so not in this package. See the
248
+ [API reference](https://developers.talki.it) for them.
249
+
250
+ ## Versions
251
+
252
+ This package follows semantic versioning of its own. `talki.API_VERSION` is
253
+ the dated API version it was generated from and sends by default; a release
254
+ that moves to a newer API version says so in the changelog.
255
+
256
+ ## Development
257
+
258
+ Most of `talki/` is generated, never edited by hand: `scripts/generate.py`
259
+ runs openapi-python-client (pinned in `pyproject.toml`) over
260
+ `openapi/talki-v1.yaml` with the templates in `codegen/templates/`, and keeps
261
+ the hand-written modules listed in the script (`_talki.py`, `_transport.py`,
262
+ `exceptions.py`, `oauth.py`, `pagination.py`, `webhooks.py`).
263
+
264
+ ```sh
265
+ uv sync
266
+ uv run scripts/pull_spec.py # fetch the current OpenAPI document
267
+ uv run scripts/generate.py # regenerate talki/ from it
268
+ uv run scripts/generate.py --check # CI: fail if talki/ is stale
269
+ uv run pytest
270
+ uv run mypy
271
+ uv run ruff check . && uv run ruff format --check .
272
+ ```
273
+
274
+ To change generated code, change a template or `codegen/config.yaml`; a
275
+ change to the API itself belongs in the API.
276
+
277
+ ## License
278
+
279
+ [MIT](https://github.com/HubtecSrl/talki-python/blob/main/LICENSE)
@@ -0,0 +1,83 @@
1
+ [project]
2
+ name = "talki"
3
+ version = "0.1.0b1"
4
+ description = "Python client for the Talki API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = [
10
+ "talki",
11
+ "api",
12
+ "client",
13
+ "voice agent",
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.23.1,<0.29.0",
29
+ "attrs>=22.2.0",
30
+ "anyio>=3.0.0",
31
+ ]
32
+
33
+ [[project.authors]]
34
+ name = "HubTec S.r.l."
35
+
36
+ [project.urls]
37
+ Homepage = "https://developers.talki.it"
38
+ Documentation = "https://developers.talki.it"
39
+ Repository = "https://github.com/HubtecSrl/talki-python"
40
+ Changelog = "https://github.com/HubtecSrl/talki-python/blob/main/CHANGELOG.md"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "mypy>=1.13",
45
+ "pytest>=8",
46
+ "ruff>=0.8",
47
+ ]
48
+ codegen = ["openapi-python-client==0.29.1"]
49
+
50
+ [tool.uv]
51
+ default-groups = [
52
+ "dev",
53
+ "codegen",
54
+ ]
55
+
56
+ [tool.uv.build-backend]
57
+ module-name = "talki"
58
+ module-root = ""
59
+
60
+ [tool.ruff]
61
+ line-length = 120
62
+ target-version = "py311"
63
+
64
+ [tool.ruff.lint]
65
+ select = [
66
+ "F",
67
+ "I",
68
+ "UP",
69
+ ]
70
+
71
+ [tool.ruff.lint.isort]
72
+ known-first-party = ["talki"]
73
+
74
+ [tool.mypy]
75
+ strict = true
76
+ packages = ["talki"]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+
81
+ [build-system]
82
+ requires = ["uv_build>=0.9,<0.13"]
83
+ build-backend = "uv_build"