hatang 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ .mypy_cache/
3
+ .pytest_cache/
4
+ __pycache__/
5
+ uv.lock
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `hatang` Python package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-31
10
+
11
+ ### Added
12
+ - First release: `Hatang` (sync) and `AsyncHatang` (asyncio) clients over httpx, with `HatangError` (`status_code`,
13
+ `name`, `message`, `retryable`).
14
+ - `chat.subscriptions`, `chat.purchases`, `chat.customers`, `chat.conversations`, `chat.products`, `chat.orders`,
15
+ `knowledge`, `emails`, `batch`, `payments`, `events.send`; typed request bodies in `hatang.types`.
16
+ - `identity_hash`, `purchase_token`, `verify_webhook` / `construct_webhook_event`, `verify_ipn` /
17
+ `construct_ipn_event`, checked against values from Ha Tang's servers and the Node SDK.
hatang-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Sota Labs
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.
hatang-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.5
2
+ Name: hatang
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Ha Tang (hatang.dev): support CRM, plans and purchases, AI knowledge, email, payments, webhooks.
5
+ Project-URL: Homepage, https://hatang.dev
6
+ Project-URL: Documentation, https://hatang.dev/docs/guides/sdks/python
7
+ Project-URL: Source, https://github.com/sota-labs/hatang/tree/main/sdks/python
8
+ Project-URL: Changelog, https://github.com/sota-labs/hatang/blob/main/sdks/python/CHANGELOG.md
9
+ Author-email: Sota Labs <hello@hatang.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: chat,crm,email,hatang,in-app purchases,support,vietqr,webhooks
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Communications :: Email
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx<1,>=0.25
23
+ Description-Content-Type: text/markdown
24
+
25
+ # hatang
26
+
27
+ The official Python SDK for [Ha Tang](https://hatang.dev): support CRM, plans and purchases on the customer's profile,
28
+ the AI assistant's knowledge, email, VietQR payments and webhooks.
29
+
30
+ ```bash
31
+ pip install hatang
32
+ ```
33
+
34
+ Python 3.9+. Sync (`Hatang`) and asyncio (`AsyncHatang`) clients, typed request bodies.
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from hatang import Hatang
40
+
41
+ hatang = Hatang() # reads HATANG_API_KEY; or Hatang("ht_…")
42
+
43
+ # What a customer pays for, from your backend (web sales included).
44
+ hatang.chat.subscriptions.upsert("web", "sub_123", {
45
+ "status": "active",
46
+ "sku": "PRO-1M", # finds the product in your catalog
47
+ "external_user_id": "user-42", # the id you pass to identify() in the chat
48
+ "current_period_end": "2026-12-01T00:00:00Z",
49
+ "amount": 99000, "currency": "VND",
50
+ })
51
+ hatang.chat.purchases.upsert("web", "order_981", {
52
+ "store_product_id": "credits.300", "sku": "XU-300", "kind": "consumable",
53
+ "external_user_id": "user-42", "amount": 29000, "currency": "VND",
54
+ })
55
+ ```
56
+
57
+ asyncio (FastAPI, aiohttp…):
58
+
59
+ ```python
60
+ from hatang import AsyncHatang
61
+
62
+ async with AsyncHatang() as hatang:
63
+ hits = await hatang.knowledge.search("hoàn tiền", limit=3)
64
+ ```
65
+
66
+ Every method returns the API's JSON as a `dict`. A call that does not succeed raises `HatangError` with `status_code`
67
+ (0 when Ha Tang was not reached), `name` (`validation_error`, `not_found`…), `message`, and `retryable` (unreachable,
68
+ 429 or 5xx).
69
+
70
+ ## What is in it
71
+
72
+ | Namespace | Methods |
73
+ |---|---|
74
+ | `chat.subscriptions` | `upsert`, `list`, `for_customer`, `purchase_token` |
75
+ | `chat.purchases` | `upsert`, `list`, `for_customer` |
76
+ | `chat.customers` | `list`, `get`, `update`, `conversations`, `export_csv` |
77
+ | `chat.conversations` | `list`, `get`, `update`, `messages`, `send`, `send_products` |
78
+ | `chat.products` | `list`, `search`, `get`, `create`, `update`, `remove` |
79
+ | `chat.orders` | `list`, `get`, `create`, `update`, `payment_link` |
80
+ | `knowledge` | `upsert`, `batch`, `sync`, `get`, `list`, `remove`, `search` |
81
+ | `emails`, `batch` | `send`, `get`, `list`, `update`, `cancel`; `batch.send` |
82
+ | `payments` | `create`, `get`, `list`, `cancel` |
83
+ | `events` | `send` |
84
+
85
+ ## Helpers
86
+
87
+ ```python
88
+ from hatang import identity_hash, purchase_token, construct_webhook_event, construct_ipn_event
89
+
90
+ # Signed chat identity: send this as `hash` with userId to identify() in the widget or the app SDKs.
91
+ identity_hash("user-42", IDENTITY_SECRET)
92
+
93
+ # StoreKit appAccountToken / Google Play obfuscatedAccountId for one of your users.
94
+ purchase_token("site_…", "user-42")
95
+
96
+ # Webhooks (whsec_… secret) and payment IPN calls: pass the raw body, before JSON parsing.
97
+ event = construct_webhook_event(request.body, request.headers, WEBHOOK_SECRET)
98
+ ipn = construct_ipn_event(request.body, request.headers, SECRET_KEY)
99
+ ```
100
+
101
+ Both raise `HatangSignatureError` when the signature does not match; `verify_webhook` / `verify_ipn` return a bool.
102
+
103
+ ## Configuration
104
+
105
+ | Argument | Environment | Default |
106
+ |---|---|---|
107
+ | `api_key` | `HATANG_API_KEY` | required |
108
+ | `base_url` | `HATANG_BASE_URL` | `https://api.hatang.dev` |
109
+ | `environment` | `HATANG_ENVIRONMENT` | the key's own |
110
+ | `timeout` | | 30 s |
111
+ | `http_client` | | an `httpx.Client` / `httpx.AsyncClient` of your own (proxies, retries, tests) |
112
+
113
+ Docs: [hatang.dev/docs](https://hatang.dev/docs/guides/sdks/python) · License: MIT
hatang-0.1.0/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # hatang
2
+
3
+ The official Python SDK for [Ha Tang](https://hatang.dev): support CRM, plans and purchases on the customer's profile,
4
+ the AI assistant's knowledge, email, VietQR payments and webhooks.
5
+
6
+ ```bash
7
+ pip install hatang
8
+ ```
9
+
10
+ Python 3.9+. Sync (`Hatang`) and asyncio (`AsyncHatang`) clients, typed request bodies.
11
+
12
+ ## Quick start
13
+
14
+ ```python
15
+ from hatang import Hatang
16
+
17
+ hatang = Hatang() # reads HATANG_API_KEY; or Hatang("ht_…")
18
+
19
+ # What a customer pays for, from your backend (web sales included).
20
+ hatang.chat.subscriptions.upsert("web", "sub_123", {
21
+ "status": "active",
22
+ "sku": "PRO-1M", # finds the product in your catalog
23
+ "external_user_id": "user-42", # the id you pass to identify() in the chat
24
+ "current_period_end": "2026-12-01T00:00:00Z",
25
+ "amount": 99000, "currency": "VND",
26
+ })
27
+ hatang.chat.purchases.upsert("web", "order_981", {
28
+ "store_product_id": "credits.300", "sku": "XU-300", "kind": "consumable",
29
+ "external_user_id": "user-42", "amount": 29000, "currency": "VND",
30
+ })
31
+ ```
32
+
33
+ asyncio (FastAPI, aiohttp…):
34
+
35
+ ```python
36
+ from hatang import AsyncHatang
37
+
38
+ async with AsyncHatang() as hatang:
39
+ hits = await hatang.knowledge.search("hoàn tiền", limit=3)
40
+ ```
41
+
42
+ Every method returns the API's JSON as a `dict`. A call that does not succeed raises `HatangError` with `status_code`
43
+ (0 when Ha Tang was not reached), `name` (`validation_error`, `not_found`…), `message`, and `retryable` (unreachable,
44
+ 429 or 5xx).
45
+
46
+ ## What is in it
47
+
48
+ | Namespace | Methods |
49
+ |---|---|
50
+ | `chat.subscriptions` | `upsert`, `list`, `for_customer`, `purchase_token` |
51
+ | `chat.purchases` | `upsert`, `list`, `for_customer` |
52
+ | `chat.customers` | `list`, `get`, `update`, `conversations`, `export_csv` |
53
+ | `chat.conversations` | `list`, `get`, `update`, `messages`, `send`, `send_products` |
54
+ | `chat.products` | `list`, `search`, `get`, `create`, `update`, `remove` |
55
+ | `chat.orders` | `list`, `get`, `create`, `update`, `payment_link` |
56
+ | `knowledge` | `upsert`, `batch`, `sync`, `get`, `list`, `remove`, `search` |
57
+ | `emails`, `batch` | `send`, `get`, `list`, `update`, `cancel`; `batch.send` |
58
+ | `payments` | `create`, `get`, `list`, `cancel` |
59
+ | `events` | `send` |
60
+
61
+ ## Helpers
62
+
63
+ ```python
64
+ from hatang import identity_hash, purchase_token, construct_webhook_event, construct_ipn_event
65
+
66
+ # Signed chat identity: send this as `hash` with userId to identify() in the widget or the app SDKs.
67
+ identity_hash("user-42", IDENTITY_SECRET)
68
+
69
+ # StoreKit appAccountToken / Google Play obfuscatedAccountId for one of your users.
70
+ purchase_token("site_…", "user-42")
71
+
72
+ # Webhooks (whsec_… secret) and payment IPN calls: pass the raw body, before JSON parsing.
73
+ event = construct_webhook_event(request.body, request.headers, WEBHOOK_SECRET)
74
+ ipn = construct_ipn_event(request.body, request.headers, SECRET_KEY)
75
+ ```
76
+
77
+ Both raise `HatangSignatureError` when the signature does not match; `verify_webhook` / `verify_ipn` return a bool.
78
+
79
+ ## Configuration
80
+
81
+ | Argument | Environment | Default |
82
+ |---|---|---|
83
+ | `api_key` | `HATANG_API_KEY` | required |
84
+ | `base_url` | `HATANG_BASE_URL` | `https://api.hatang.dev` |
85
+ | `environment` | `HATANG_ENVIRONMENT` | the key's own |
86
+ | `timeout` | | 30 s |
87
+ | `http_client` | | an `httpx.Client` / `httpx.AsyncClient` of your own (proxies, retries, tests) |
88
+
89
+ Docs: [hatang.dev/docs](https://hatang.dev/docs/guides/sdks/python) · License: MIT
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "hatang"
3
+ version = "0.1.0"
4
+ description = "Official Python SDK for Ha Tang (hatang.dev): support CRM, plans and purchases, AI knowledge, email, payments, webhooks."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.9"
8
+ authors = [{ name = "Sota Labs", email = "hello@hatang.dev" }]
9
+ keywords = ["hatang", "support", "crm", "chat", "email", "vietqr", "webhooks", "in-app purchases"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Topic :: Communications :: Email",
17
+ "Topic :: Software Development :: Libraries",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = ["httpx>=0.25,<1"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://hatang.dev"
24
+ Documentation = "https://hatang.dev/docs/guides/sdks/python"
25
+ Source = "https://github.com/sota-labs/hatang/tree/main/sdks/python"
26
+ Changelog = "https://github.com/sota-labs/hatang/blob/main/sdks/python/CHANGELOG.md"
27
+
28
+ [dependency-groups]
29
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.10"]
30
+
31
+ [build-system]
32
+ requires = ["hatchling>=1.25"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/hatang"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ asyncio_mode = "auto"
41
+
42
+ [tool.mypy]
43
+ strict = true
44
+ files = ["src/hatang"]
@@ -0,0 +1,31 @@
1
+ """Official Python SDK for Ha Tang (hatang.dev).
2
+
3
+ from hatang import Hatang
4
+
5
+ hatang = Hatang() # HATANG_API_KEY
6
+ hatang.chat.subscriptions.upsert("web", "sub_123", {"status": "active", "external_user_id": "u1"})
7
+ """
8
+
9
+ from ._client import DEFAULT_BASE_URL
10
+ from ._errors import HatangError, HatangSignatureError
11
+ from ._hatang import AsyncHatang, Hatang
12
+ from ._version import VERSION
13
+ from .helpers import identity_hash, purchase_token
14
+ from .webhooks import construct_ipn_event, construct_webhook_event, verify_ipn, verify_webhook
15
+
16
+ __version__ = VERSION
17
+
18
+ __all__ = [
19
+ "DEFAULT_BASE_URL",
20
+ "VERSION",
21
+ "AsyncHatang",
22
+ "Hatang",
23
+ "HatangError",
24
+ "HatangSignatureError",
25
+ "construct_ipn_event",
26
+ "construct_webhook_event",
27
+ "identity_hash",
28
+ "purchase_token",
29
+ "verify_ipn",
30
+ "verify_webhook",
31
+ ]
@@ -0,0 +1,150 @@
1
+ """The HTTP layer shared by ``Hatang`` and ``AsyncHatang``: headers, query strings, errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ from typing import Any, Dict, Mapping, Optional, Union
8
+ from urllib.parse import quote
9
+
10
+ import httpx
11
+
12
+ from ._errors import HatangError
13
+ from ._version import VERSION
14
+
15
+ DEFAULT_BASE_URL = "https://api.hatang.dev"
16
+ DEFAULT_TIMEOUT = 30.0
17
+
18
+ JSON = Dict[str, Any]
19
+ Query = Mapping[str, Any]
20
+
21
+
22
+ def seg(value: str) -> str:
23
+ """One path segment: ids may hold ``/`` (Play purchase tokens, your own order ids)."""
24
+ return quote(str(value), safe="")
25
+
26
+
27
+ def query_params(params: Optional[Query]) -> Dict[str, str]:
28
+ """Query parameters as the API reads them: None dropped, booleans as true/false, lists comma-joined."""
29
+ out: Dict[str, str] = {}
30
+ for key, value in (params or {}).items():
31
+ if value is None:
32
+ continue
33
+ if isinstance(value, bool):
34
+ out[key] = "true" if value else "false"
35
+ elif isinstance(value, (list, tuple)):
36
+ out[key] = ",".join(str(v) for v in value)
37
+ else:
38
+ out[key] = str(value)
39
+ return out
40
+
41
+
42
+ class _Config:
43
+ def __init__(
44
+ self,
45
+ api_key: Optional[str],
46
+ base_url: Optional[str],
47
+ environment: Optional[str],
48
+ user_agent: Optional[str],
49
+ ) -> None:
50
+ key = api_key or os.environ.get("HATANG_API_KEY")
51
+ if not key:
52
+ raise ValueError('Missing API key. Pass it to the constructor: Hatang("ht_123"), or set HATANG_API_KEY')
53
+ self.api_key = key
54
+ self.base_url = (base_url or os.environ.get("HATANG_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
55
+ self.environment = environment or os.environ.get("HATANG_ENVIRONMENT")
56
+ self.user_agent = user_agent or f"hatang-sdk-python/{VERSION} (python/{platform.python_version()})"
57
+
58
+ def headers(self, has_body: bool, extra: Optional[Mapping[str, str]]) -> Dict[str, str]:
59
+ h = {"Authorization": f"Bearer {self.api_key}", "User-Agent": self.user_agent, "Accept": "application/json"}
60
+ if self.environment:
61
+ h["Hatang-Environment"] = self.environment
62
+ if has_body:
63
+ h["Content-Type"] = "application/json"
64
+ h.update(extra or {})
65
+ return h
66
+
67
+
68
+ def _parse(res: httpx.Response) -> Any:
69
+ try:
70
+ body: Any = res.json() if res.content else None
71
+ except ValueError:
72
+ body = None
73
+ if res.is_success:
74
+ return body if body is not None else {}
75
+ err: Mapping[str, Any] = body if isinstance(body, dict) else {}
76
+ raise HatangError(
77
+ int(err.get("statusCode") or res.status_code),
78
+ str(err.get("name") or "application_error"),
79
+ str(err.get("message") or res.reason_phrase or "Request failed"),
80
+ )
81
+
82
+
83
+ def _unreachable(error: Exception) -> HatangError:
84
+ return HatangError(0, "application_error", f"Unable to reach the API: {error}")
85
+
86
+
87
+ RequestHeaders = Optional[Mapping[str, str]]
88
+ Body = Union[JSON, list, None] # type: ignore[type-arg]
89
+
90
+
91
+ class SyncTransport:
92
+ def __init__(self, config: _Config, timeout: float, http_client: Optional[httpx.Client]) -> None:
93
+ self._config = config
94
+ self._http = http_client or httpx.Client(timeout=timeout)
95
+
96
+ def request(self, method: str, path: str, body: Body = None, query: Optional[Query] = None, headers: RequestHeaders = None) -> Any:
97
+ try:
98
+ res = self._http.request(
99
+ method,
100
+ f"{self._config.base_url}{path}",
101
+ params=query_params(query),
102
+ json=body,
103
+ headers=self._config.headers(body is not None, headers),
104
+ )
105
+ except httpx.HTTPError as error:
106
+ raise _unreachable(error) from error
107
+ return _parse(res)
108
+
109
+ def text(self, path: str, query: Optional[Query] = None) -> Any:
110
+ try:
111
+ res = self._http.get(f"{self._config.base_url}{path}", params=query_params(query), headers=self._config.headers(False, None))
112
+ except httpx.HTTPError as error:
113
+ raise _unreachable(error) from error
114
+ if not res.is_success:
115
+ _parse(res)
116
+ return res.text
117
+
118
+ def close(self) -> None:
119
+ self._http.close()
120
+
121
+
122
+ class AsyncTransport:
123
+ def __init__(self, config: _Config, timeout: float, http_client: Optional[httpx.AsyncClient]) -> None:
124
+ self._config = config
125
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
126
+
127
+ async def request(self, method: str, path: str, body: Body = None, query: Optional[Query] = None, headers: RequestHeaders = None) -> Any:
128
+ try:
129
+ res = await self._http.request(
130
+ method,
131
+ f"{self._config.base_url}{path}",
132
+ params=query_params(query),
133
+ json=body,
134
+ headers=self._config.headers(body is not None, headers),
135
+ )
136
+ except httpx.HTTPError as error:
137
+ raise _unreachable(error) from error
138
+ return _parse(res)
139
+
140
+ async def text(self, path: str, query: Optional[Query] = None) -> Any:
141
+ try:
142
+ res = await self._http.get(f"{self._config.base_url}{path}", params=query_params(query), headers=self._config.headers(False, None))
143
+ except httpx.HTTPError as error:
144
+ raise _unreachable(error) from error
145
+ if not res.is_success:
146
+ _parse(res)
147
+ return res.text
148
+
149
+ async def aclose(self) -> None:
150
+ await self._http.aclose()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class HatangError(Exception):
5
+ """An API call that did not succeed.
6
+
7
+ ``status_code`` is the HTTP status, or 0 when Ha Tang was not reached (network error, timeout).
8
+ ``name`` is the API's error code (``validation_error``, ``not_found``, ``rate_limit_exceeded``…).
9
+ """
10
+
11
+ def __init__(self, status_code: int, name: str, message: str) -> None:
12
+ super().__init__(f"{status_code} {name}: {message}")
13
+ self.status_code = status_code
14
+ self.name = name
15
+ self.message = message
16
+
17
+ @property
18
+ def retryable(self) -> bool:
19
+ """True for outages worth retrying: unreachable, rate limited, or a server error."""
20
+ return self.status_code == 0 or self.status_code == 429 or self.status_code >= 500
21
+
22
+
23
+ class HatangSignatureError(Exception):
24
+ """A webhook or IPN whose signature (or timestamp) does not match."""
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from types import TracebackType
4
+ from typing import Any, Awaitable, Optional, Type
5
+
6
+ import httpx
7
+
8
+ from ._client import DEFAULT_TIMEOUT, JSON, AsyncTransport, SyncTransport, _Config
9
+ from ._resources import Batch, Chat, Emails, Events, Knowledge, Payments
10
+
11
+
12
+ class Hatang:
13
+ """The Ha Tang API.
14
+
15
+ ``Hatang("ht_…")``, or set ``HATANG_API_KEY``. Every method returns the API's JSON as a dict and raises
16
+ ``HatangError`` when the call does not succeed. Use it as a context manager, or call ``close()``, to release
17
+ its connections.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ api_key: Optional[str] = None,
23
+ *,
24
+ base_url: Optional[str] = None,
25
+ environment: Optional[str] = None,
26
+ timeout: float = DEFAULT_TIMEOUT,
27
+ http_client: Optional[httpx.Client] = None,
28
+ user_agent: Optional[str] = None,
29
+ ) -> None:
30
+ config = _Config(api_key, base_url, environment, user_agent)
31
+ self.base_url = config.base_url
32
+ self._transport = SyncTransport(config, timeout, http_client)
33
+ request, text = self._transport.request, self._transport.text
34
+ self.emails: Emails[JSON, str] = Emails(request, text)
35
+ self.batch: Batch[JSON, str] = Batch(request, text)
36
+ self.knowledge: Knowledge[JSON, str] = Knowledge(request, text)
37
+ self.chat: Chat[JSON, str] = Chat(request, text)
38
+ self.payments: Payments[JSON, str] = Payments(request, text)
39
+ self.events: Events[JSON, str] = Events(request, text)
40
+
41
+ def close(self) -> None:
42
+ self._transport.close()
43
+
44
+ def __enter__(self) -> "Hatang":
45
+ return self
46
+
47
+ def __exit__(self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], tb: Optional[TracebackType]) -> None:
48
+ self.close()
49
+
50
+
51
+ class AsyncHatang:
52
+ """The Ha Tang API for asyncio (FastAPI, aiohttp…): the same namespaces as ``Hatang``, every method awaited."""
53
+
54
+ def __init__(
55
+ self,
56
+ api_key: Optional[str] = None,
57
+ *,
58
+ base_url: Optional[str] = None,
59
+ environment: Optional[str] = None,
60
+ timeout: float = DEFAULT_TIMEOUT,
61
+ http_client: Optional[httpx.AsyncClient] = None,
62
+ user_agent: Optional[str] = None,
63
+ ) -> None:
64
+ config = _Config(api_key, base_url, environment, user_agent)
65
+ self.base_url = config.base_url
66
+ self._transport = AsyncTransport(config, timeout, http_client)
67
+ request, text = self._transport.request, self._transport.text
68
+ self.emails: Emails[Awaitable[JSON], Awaitable[str]] = Emails(request, text)
69
+ self.batch: Batch[Awaitable[JSON], Awaitable[str]] = Batch(request, text)
70
+ self.knowledge: Knowledge[Awaitable[JSON], Awaitable[str]] = Knowledge(request, text)
71
+ self.chat: Chat[Awaitable[JSON], Awaitable[str]] = Chat(request, text)
72
+ self.payments: Payments[Awaitable[JSON], Awaitable[str]] = Payments(request, text)
73
+ self.events: Events[Awaitable[JSON], Awaitable[str]] = Events(request, text)
74
+
75
+ async def aclose(self) -> None:
76
+ await self._transport.aclose()
77
+
78
+ async def __aenter__(self) -> "AsyncHatang":
79
+ return self
80
+
81
+ async def __aexit__(self, *args: Any) -> None:
82
+ await self.aclose()