adsefid 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 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 5
8
+ - package-ecosystem: github-actions
9
+ directory: /
10
+ schedule:
11
+ interval: weekly
12
+ open-pull-requests-limit: 5
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.10"
26
+
27
+ - run: make deps
28
+ - run: make lint
29
+ - run: make build
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ dist/
8
+ *.egg-info/
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .pytest_cache/
16
+ .tox/
17
+ .coverage
18
+ htmlcov/
19
+ *.log
20
+ .DS_Store
21
+ .idea/
22
+ .vscode/
23
+ *.iml
@@ -0,0 +1,89 @@
1
+ # AGENTS.md — adsefid Python SDK
2
+
3
+ ## Scope
4
+
5
+ This repository is the Python client SDK for the adsefid.com SMS Web Service API (package:
6
+ `adsefid`), independently versioned and published with its own `pyproject.toml`. Equivalent SDKs
7
+ exist for the same API in sibling repositories (`sdk-dotnet`, `sdk-js`, `sdk-php`, `sdk-go`); a
8
+ behavior change here should generally be considered for parity there.
9
+
10
+ ## Source of truth
11
+
12
+ The API surface (endpoints, field names, types, validation rules, enums, example payloads,
13
+ webhook behavior) is defined by the published adsefid.com SMS Web Service API documentation.
14
+ This SDK is verified against doc version v1.11.0. Re-read the relevant documentation before
15
+ changing any endpoint, request/response model, or enum. The SDK follows independent Semantic
16
+ Versioning from `pyproject.toml`; never copy the API-document version into package metadata.
17
+ Record both versions in the README.
18
+
19
+ A small number of facts below are empirically observed behaviors of the live API that are easy
20
+ to get wrong from a literal reading of the documentation's prose or pseudo-code. Trust these
21
+ notes over an ambiguous doc reading:
22
+
23
+ - Webhook signatures are plain Base64, not hex-then-Base64. The signature is HMAC-SHA256 over
24
+ the literal string `"{timestamp}.{raw_body}"`, and the raw digest bytes are Base64-encoded
25
+ directly — there is no intermediate hex-encoding step, even though a literal reading of some
26
+ spec pseudo-code can suggest one. See `src/adsefid/webhooks/verifier.py`.
27
+ - `TemplateParameterType` has an undocumented third value in the wild. The documented, supported
28
+ public set is `{string, number}`. The live API has been observed to also emit a `url` value for
29
+ some templates; this SDK intentionally models only the two documented values — do not add
30
+ support for it without first confirming it against current, documented API behavior. See
31
+ `src/adsefid/enums.py`.
32
+ - `error.details` shape varies per endpoint and is intentionally untyped. It may be a validation
33
+ map, a bulk/P2P per-item list, a cancel-specific map, or absent entirely — never give it a
34
+ strong type; decode it defensively per endpoint if you need it.
35
+
36
+ ## Architecture map
37
+
38
+ ```
39
+ src/adsefid/
40
+ ├── __init__.py public API re-exports
41
+ ├── py.typed PEP 561 marker — do not remove
42
+ ├── client.py AdsefidClient (sync, wraps httpx.Client)
43
+ ├── async_client.py AdsefidAsyncClient (async, wraps httpx.AsyncClient)
44
+ ├── _base.py shared request-building / envelope-parsing / error-raising (used by both clients)
45
+ ├── config.py ClientConfig
46
+ ├── exceptions.py full exception hierarchy
47
+ ├── enums.py LineSelector, WebServiceMessageStatus, WebServiceResponseCode, TemplateState, TemplateParameterType
48
+ ├── _serialization.py to_dict/from_dict helpers, ISO-8601 datetime parse/format, local_id regex + validators, CSV join
49
+ ├── resources/
50
+ │ ├── sms.py SmsResource (sync) + AsyncSmsResource (async) — 7 ops each
51
+ │ ├── messenger.py MessengerResource + AsyncMessengerResource — 7 ops each (incl. upload_file)
52
+ │ └── user.py UserResource + AsyncUserResource — 4 ops each
53
+ ├── models/
54
+ │ ├── common.py shared envelope/status/cancel dataclasses reused by sms + messenger
55
+ │ ├── sms.py request/response dataclasses for the 7 SMS ops
56
+ │ ├── messenger.py request/response dataclasses for the 7 messenger ops
57
+ │ └── user.py request/response dataclasses for the 4 user ops
58
+ └── webhooks/
59
+ ├── verifier.py verify_and_parse_webhook
60
+ ├── events.py WebhookEvent union + 3 concrete dataclasses
61
+ └── headers.py WebhookHeaders/WebhookEventType constants — use instead of typing header/type strings
62
+ ```
63
+
64
+ ## Adding a new endpoint
65
+
66
+ 1. Add the request/response `@dataclass(frozen=True, slots=True)` types to the right `models/<area>.py` (or `models/common.py` if the shape is shared across SMS and messenger, e.g. cancel/status). Hand-write `to_dict`/`from_dict` — no metaprogramming.
67
+ 2. Add one sync method to the resource class in `resources/<area>.py`, and the structurally identical `async def` method to the `Async<Area>Resource` class in the same file. The two classes must stay parallel: same method names, same parameter order, same validation calls, same shape of return.
68
+ 3. Any client-side pre-flight rule (max length, required field, `local_id` shape, count limits) goes in `_serialization.py` as a small named validator function and is called from the resource method *before* the request is built — never inline `if` checks scattered across resource methods.
69
+ 4. Re-export anything new that belongs in the public surface from `adsefid/__init__.py`.
70
+
71
+ ## Hard rules
72
+
73
+ - **No tests, ever.** Do not add a `tests/` directory, doctest, or pytest dependency to this repo.
74
+ - **No pydantic, no reflection-based validation/serialization.** Every dataclass hand-writes its own `to_dict`/`from_dict`. If two models share shape, factor the shared dataclass into `models/common.py` — don't reach for a validation library.
75
+ - **No magic string/int literals.** Any code value that appears in the doc's enum tables belongs in `enums.py`. Any other repeated constant (max lengths, limits, header names) is a named module-level constant, not a literal repeated at call sites.
76
+ - **Docstrings on the public API, minimal comments elsewhere.** Every public class/function (clients, resources, exceptions, enums, `verify_and_parse_webhook`) needs a PEP 257 docstring with real content — not a restatement of its name. Internal/private (`_`-prefixed) code stays uncommented except where a genuinely non-obvious constraint requires a note (e.g. the webhook digest-vs-hex behavior, the permissive-enum-parsing rationale). Don't narrate what the code obviously does.
77
+ - **Raise on error, always — never introduce a Result/Either type.** Every resource method either returns a typed success dataclass or raises from the `exceptions.py` hierarchy. Partial-success bulk/P2P responses are still normal typed returns (they're HTTP 200 successes with per-item status), not exceptions.
78
+ - **Keep sync and async clients/resources behaviorally identical.** Same validation, same error mapping, same defaults. If you touch one, touch the other in the same change.
79
+ - **No retry logic anywhere in this SDK.** Every request is a single attempt.
80
+ - `WebServiceMessageStatus` and `WebServiceResponseCode` are the two enums most likely to grow ahead of doc updates — they're parsed permissively (unknown int falls back to a raw int field rather than raising). Don't "fix" this into a hard `IntEnum(value)` call that would crash on a new server-side code.
81
+
82
+ ## Commands
83
+
84
+ ```bash
85
+ pip install -e ".[dev]" # installs ruff + mypy + build into this repo's venv
86
+ make lint # ruff check + ruff format --check + mypy
87
+ make fmt # ruff format .
88
+ make build # python -m build (sdist + wheel)
89
+ ```
@@ -0,0 +1,89 @@
1
+ # AGENTS.md — adsefid Python SDK
2
+
3
+ ## Scope
4
+
5
+ This repository is the Python client SDK for the adsefid.com SMS Web Service API (package:
6
+ `adsefid`), independently versioned and published with its own `pyproject.toml`. Equivalent SDKs
7
+ exist for the same API in sibling repositories (`sdk-dotnet`, `sdk-js`, `sdk-php`, `sdk-go`); a
8
+ behavior change here should generally be considered for parity there.
9
+
10
+ ## Source of truth
11
+
12
+ The API surface (endpoints, field names, types, validation rules, enums, example payloads,
13
+ webhook behavior) is defined by the published adsefid.com SMS Web Service API documentation.
14
+ This SDK is verified against doc version v1.11.0. Re-read the relevant documentation before
15
+ changing any endpoint, request/response model, or enum. The SDK follows independent Semantic
16
+ Versioning from `pyproject.toml`; never copy the API-document version into package metadata.
17
+ Record both versions in the README.
18
+
19
+ A small number of facts below are empirically observed behaviors of the live API that are easy
20
+ to get wrong from a literal reading of the documentation's prose or pseudo-code. Trust these
21
+ notes over an ambiguous doc reading:
22
+
23
+ - Webhook signatures are plain Base64, not hex-then-Base64. The signature is HMAC-SHA256 over
24
+ the literal string `"{timestamp}.{raw_body}"`, and the raw digest bytes are Base64-encoded
25
+ directly — there is no intermediate hex-encoding step, even though a literal reading of some
26
+ spec pseudo-code can suggest one. See `src/adsefid/webhooks/verifier.py`.
27
+ - `TemplateParameterType` has an undocumented third value in the wild. The documented, supported
28
+ public set is `{string, number}`. The live API has been observed to also emit a `url` value for
29
+ some templates; this SDK intentionally models only the two documented values — do not add
30
+ support for it without first confirming it against current, documented API behavior. See
31
+ `src/adsefid/enums.py`.
32
+ - `error.details` shape varies per endpoint and is intentionally untyped. It may be a validation
33
+ map, a bulk/P2P per-item list, a cancel-specific map, or absent entirely — never give it a
34
+ strong type; decode it defensively per endpoint if you need it.
35
+
36
+ ## Architecture map
37
+
38
+ ```
39
+ src/adsefid/
40
+ ├── __init__.py public API re-exports
41
+ ├── py.typed PEP 561 marker — do not remove
42
+ ├── client.py AdsefidClient (sync, wraps httpx.Client)
43
+ ├── async_client.py AdsefidAsyncClient (async, wraps httpx.AsyncClient)
44
+ ├── _base.py shared request-building / envelope-parsing / error-raising (used by both clients)
45
+ ├── config.py ClientConfig
46
+ ├── exceptions.py full exception hierarchy
47
+ ├── enums.py LineSelector, WebServiceMessageStatus, WebServiceResponseCode, TemplateState, TemplateParameterType
48
+ ├── _serialization.py to_dict/from_dict helpers, ISO-8601 datetime parse/format, local_id regex + validators, CSV join
49
+ ├── resources/
50
+ │ ├── sms.py SmsResource (sync) + AsyncSmsResource (async) — 7 ops each
51
+ │ ├── messenger.py MessengerResource + AsyncMessengerResource — 7 ops each (incl. upload_file)
52
+ │ └── user.py UserResource + AsyncUserResource — 4 ops each
53
+ ├── models/
54
+ │ ├── common.py shared envelope/status/cancel dataclasses reused by sms + messenger
55
+ │ ├── sms.py request/response dataclasses for the 7 SMS ops
56
+ │ ├── messenger.py request/response dataclasses for the 7 messenger ops
57
+ │ └── user.py request/response dataclasses for the 4 user ops
58
+ └── webhooks/
59
+ ├── verifier.py verify_and_parse_webhook
60
+ ├── events.py WebhookEvent union + 3 concrete dataclasses
61
+ └── headers.py WebhookHeaders/WebhookEventType constants — use instead of typing header/type strings
62
+ ```
63
+
64
+ ## Adding a new endpoint
65
+
66
+ 1. Add the request/response `@dataclass(frozen=True, slots=True)` types to the right `models/<area>.py` (or `models/common.py` if the shape is shared across SMS and messenger, e.g. cancel/status). Hand-write `to_dict`/`from_dict` — no metaprogramming.
67
+ 2. Add one sync method to the resource class in `resources/<area>.py`, and the structurally identical `async def` method to the `Async<Area>Resource` class in the same file. The two classes must stay parallel: same method names, same parameter order, same validation calls, same shape of return.
68
+ 3. Any client-side pre-flight rule (max length, required field, `local_id` shape, count limits) goes in `_serialization.py` as a small named validator function and is called from the resource method *before* the request is built — never inline `if` checks scattered across resource methods.
69
+ 4. Re-export anything new that belongs in the public surface from `adsefid/__init__.py`.
70
+
71
+ ## Hard rules
72
+
73
+ - **No tests, ever.** Do not add a `tests/` directory, doctest, or pytest dependency to this repo.
74
+ - **No pydantic, no reflection-based validation/serialization.** Every dataclass hand-writes its own `to_dict`/`from_dict`. If two models share shape, factor the shared dataclass into `models/common.py` — don't reach for a validation library.
75
+ - **No magic string/int literals.** Any code value that appears in the doc's enum tables belongs in `enums.py`. Any other repeated constant (max lengths, limits, header names) is a named module-level constant, not a literal repeated at call sites.
76
+ - **Docstrings on the public API, minimal comments elsewhere.** Every public class/function (clients, resources, exceptions, enums, `verify_and_parse_webhook`) needs a PEP 257 docstring with real content — not a restatement of its name. Internal/private (`_`-prefixed) code stays uncommented except where a genuinely non-obvious constraint requires a note (e.g. the webhook digest-vs-hex behavior, the permissive-enum-parsing rationale). Don't narrate what the code obviously does.
77
+ - **Raise on error, always — never introduce a Result/Either type.** Every resource method either returns a typed success dataclass or raises from the `exceptions.py` hierarchy. Partial-success bulk/P2P responses are still normal typed returns (they're HTTP 200 successes with per-item status), not exceptions.
78
+ - **Keep sync and async clients/resources behaviorally identical.** Same validation, same error mapping, same defaults. If you touch one, touch the other in the same change.
79
+ - **No retry logic anywhere in this SDK.** Every request is a single attempt.
80
+ - `WebServiceMessageStatus` and `WebServiceResponseCode` are the two enums most likely to grow ahead of doc updates — they're parsed permissively (unknown int falls back to a raw int field rather than raising). Don't "fix" this into a hard `IntEnum(value)` call that would crash on a new server-side code.
81
+
82
+ ## Commands
83
+
84
+ ```bash
85
+ pip install -e ".[dev]" # installs ruff + mypy + build into this repo's venv
86
+ make lint # ruff check + ruff format --check + mypy
87
+ make fmt # ruff format .
88
+ make build # python -m build (sdist + wheel)
89
+ ```
adsefid-0.1.0/Makefile ADDED
@@ -0,0 +1,9 @@
1
+ .PHONY: deps fmt lint build
2
+ deps:
3
+ pip install -e ".[dev]"
4
+ fmt:
5
+ ruff format .
6
+ lint:
7
+ ruff check . && ruff format --check . && mypy src/adsefid
8
+ build:
9
+ python -m build
adsefid-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.5
2
+ Name: adsefid
3
+ Version: 0.1.0
4
+ Summary: Python client SDK for the adsefid.com SMS Web Service API
5
+ Project-URL: Homepage, https://github.com/adsefid/sdk-python
6
+ Project-URL: Repository, https://github.com/adsefid/sdk-python
7
+ Project-URL: Issues, https://github.com/adsefid/sdk-python/issues
8
+ Author: adsefid.com
9
+ License: Proprietary
10
+ Keywords: adsefid,messenger,sms,webservice
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27
19
+ Provides-Extra: dev
20
+ Requires-Dist: build>=1.2; extra == 'dev'
21
+ Requires-Dist: mypy>=1.11; extra == 'dev'
22
+ Requires-Dist: ruff>=0.6; extra == 'dev'
23
+ Provides-Extra: examples
24
+ Requires-Dist: flask; extra == 'examples'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # adsefid
28
+
29
+ [![CI](https://github.com/adsefid/sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/adsefid/sdk-python/actions/workflows/ci.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/adsefid.svg)](https://pypi.org/project/adsefid/)
31
+
32
+ Python client SDK for the [adsefid.com](https://adsefid.com) SMS Web Service API — SMS, Messenger (Rubika/Bale/etc.), and account/user endpoints, plus outgoing webhook signature verification.
33
+
34
+ ## Requirements
35
+
36
+ - Python 3.10+
37
+ - [`httpx`](https://www.python-httpx.org/) (the only runtime dependency)
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install adsefid
43
+ ```
44
+
45
+ ## Quickstart
46
+
47
+ ### Sync
48
+
49
+ ```python
50
+ import os
51
+
52
+ from adsefid import AdsefidClient
53
+ from adsefid.models.sms import SendSingleSmsRequest
54
+
55
+ client = AdsefidClient(api_key=os.environ["ADSEFID_API_KEY"])
56
+
57
+ result = client.sms.send_single(
58
+ SendSingleSmsRequest(
59
+ receptor="98912xxxxxxx",
60
+ line_number="3000xxxx",
61
+ message="Hello from adsefid",
62
+ )
63
+ )
64
+ print(result.message_id, result.status)
65
+
66
+ client.close()
67
+ ```
68
+
69
+ ### Async
70
+
71
+ ```python
72
+ import asyncio
73
+ import os
74
+
75
+ from adsefid import AdsefidAsyncClient
76
+ from adsefid.models.sms import SendSingleSmsRequest
77
+
78
+
79
+ async def main() -> None:
80
+ async with AdsefidAsyncClient(api_key=os.environ["ADSEFID_API_KEY"]) as client:
81
+ result = await client.sms.send_single(
82
+ SendSingleSmsRequest(
83
+ receptor="98912xxxxxxx",
84
+ line_number="3000xxxx",
85
+ message="Hello from adsefid",
86
+ )
87
+ )
88
+ print(result.message_id, result.status)
89
+
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ `AdsefidClient` also supports the `with` statement (shown above via explicit `close()`); `AdsefidAsyncClient` supports `async with`.
95
+
96
+ ## Authentication
97
+
98
+ The SDK never reads environment variables itself — pass the API key explicitly:
99
+
100
+ ```python
101
+ import os
102
+ from adsefid import AdsefidClient
103
+
104
+ client = AdsefidClient(api_key=os.environ["ADSEFID_API_KEY"])
105
+ ```
106
+
107
+ ## Configuration
108
+
109
+ ```python
110
+ from adsefid import AdsefidClient
111
+ import httpx
112
+
113
+ client = AdsefidClient(
114
+ api_key="...",
115
+ base_url="https://api.adsefid.com", # override for a different environment
116
+ timeout=30.0, # seconds, passed to httpx
117
+ http_client=httpx.Client(...), # optional: inject your own configured httpx.Client
118
+ )
119
+ ```
120
+
121
+ If you pass your own `http_client`/`AdsefidAsyncClient(http_client=...)`, the SDK will not close it for you when `.close()`/`.aclose()` is called — you own its lifecycle.
122
+
123
+ ## Resource reference
124
+
125
+ | Resource | SDK method | HTTP endpoint | Notes |
126
+ |---|---|---|---|
127
+ | `client.sms` | `send_single(request)` | `POST /v1/sms/single` | |
128
+ | `client.sms` | `send_bulk(request)` | `POST /v1/sms/bulk` | Partial success is a normal typed return, not an exception |
129
+ | `client.sms` | `send_p2p(request)` | `POST /v1/sms/p2p` | Partial success is a normal typed return, not an exception |
130
+ | `client.sms` | `send_template(request)` | `POST /v1/sms/template` | |
131
+ | `client.sms` | `get_status(message_ids=..., local_ids=...)` | `GET /v1/sms/status` | |
132
+ | `client.sms` | `cancel(request)` | `POST /v1/sms/cancel` | |
133
+ | `client.sms` | `get_received(line_number=..., count=..., since=...)` | `GET /v1/sms/receive` | |
134
+ | `client.messenger` | `send_single(request)` | `POST /v1/messenger/single` | |
135
+ | `client.messenger` | `send_bulk(request)` | `POST /v1/messenger/bulk` | Partial success is a normal typed return, not an exception |
136
+ | `client.messenger` | `send_p2p(request)` | `POST /v1/messenger/p2p` | Partial success is a normal typed return, not an exception |
137
+ | `client.messenger` | `upload_file(file, filename=..., content_type=...)` | `POST /v1/messenger/file` | Accepts a path, bytes, or an open file object |
138
+ | `client.messenger` | `cancel(request)` | `POST /v1/messenger/cancel` | |
139
+ | `client.messenger` | `send_template(request)` | `POST /v1/messenger/template` | |
140
+ | `client.messenger` | `get_status(message_ids=..., local_ids=...)` | `GET /v1/messenger/status` | |
141
+ | `client.user` | `get_info()` | `GET /v1/user/info` | |
142
+ | `client.user` | `get_lines()` | `GET /v1/user/lines` | |
143
+ | `client.user` | `get_profiles()` | `GET /v1/user/profiles` | |
144
+ | `client.user` | `get_templates(state=..., skip=..., take=...)` | `GET /v1/user/templates` | |
145
+
146
+ `AdsefidAsyncClient` exposes the identical surface with `await`-able methods (`client.sms.send_single(...)`, etc.).
147
+
148
+ Every method takes/returns hand-written `@dataclass(frozen=True, slots=True)` request/response types from `adsefid.models.*` whose fields mirror the API's snake_case JSON exactly.
149
+
150
+ ## Error handling
151
+
152
+ Every resource method **raises** on a non-success API response or a non-2xx HTTP status — it never returns a Result/Either type. Bulk and P2P responses with mixed per-item outcomes are still normal successful returns (HTTP 200, `status: "success"`); check each item's own `status`/`raw_status` field.
153
+
154
+ ```python
155
+ from adsefid import AdsefidApiError, AdsefidRateLimitError, AdsefidValidationError
156
+
157
+ try:
158
+ result = client.sms.send_single(request)
159
+ except AdsefidValidationError as e:
160
+ # Failed a client-side pre-flight check (e.g. bad local_id, message too long) — no network call was made.
161
+ print("invalid request:", e)
162
+ except AdsefidRateLimitError as e:
163
+ # WebServiceResponseCode 2035 (MESSAGE_LIMIT_REACHED) or 2036 (REQUEST_LIMIT_REACHED),
164
+ # or a bare HTTP 429 with an unparseable body.
165
+ print("rate limited:", e.code, e.name)
166
+ except AdsefidApiError as e:
167
+ print("API error:", e.code, e.name, e.http_status_code, e.details)
168
+ ```
169
+
170
+ ### Rate limits
171
+
172
+ The default sending limit is 500 units/second shared across SMS and Messenger traffic (SMS counts by segment). Codes `2035`, `2036`, and bare HTTP `429` responses surface as `AdsefidRateLimitError`.
173
+
174
+ ## Enums
175
+
176
+ All enums live in `adsefid.enums` and are exported from the top-level package:
177
+
178
+ - `LineSelector` (`IntEnum`, 0-5) — see doc §3.1
179
+ - `WebServiceMessageStatus` (`IntEnum`, 1000-1999) — see doc §3.2. Parsed **permissively**: response dataclasses expose both a `status: WebServiceMessageStatus | None` field and a `raw_status: int` field, so an unrecognized future status code never crashes parsing.
180
+ - `WebServiceResponseCode` (`IntEnum`, 2000-2045) — see doc §3.4. `AdsefidApiError.code` is the enum member when recognized, otherwise the raw `int`.
181
+ - `TemplateState` (`str, Enum`: `pendingapproval` / `approved` / `rejected`) — see doc §3.5
182
+ - `TemplateParameterType` (`str, Enum`: `string` / `number`) — see doc §3.6. This is the doc's complete *documented* public set; the live server has been observed to also emit an undocumented `url` value which is intentionally not exposed here.
183
+
184
+ Free-form server strings with no complete documented enum (`messenger`, e.g. `"rubika"`/`"bale"`; `account_status`, e.g. `"active"`) are plain `str` fields, not enums.
185
+
186
+ ## File upload example
187
+
188
+ ```python
189
+ from adsefid.resources.messenger import MessengerResource # via client.messenger
190
+
191
+ file_result = client.messenger.upload_file("./brochure.pdf", content_type="application/pdf")
192
+ print(file_result.file_id)
193
+
194
+ # Also accepts raw bytes or an already-open binary file object:
195
+ with open("./brochure.pdf", "rb") as f:
196
+ file_result = client.messenger.upload_file(
197
+ f, filename="brochure.pdf", content_type="application/pdf"
198
+ )
199
+ ```
200
+
201
+ ## Webhook verification
202
+
203
+ The platform signs outgoing webhooks as `X-Atlas-Webhook-Signature: v1=<base64_hmac_sha256>` over `f"{timestamp}.{raw_body}"`, keyed with your per-endpoint webhook secret. Always verify on the **raw** request body bytes, before any JSON parsing your framework might have already done.
204
+
205
+ ```python
206
+ from flask import Flask, request
207
+
208
+ from adsefid import AdsefidWebhookVerificationError, verify_and_parse_webhook
209
+ from adsefid.webhooks import WebhookEventType, WebhookHeaders
210
+
211
+ app = Flask(__name__)
212
+ WEBHOOK_SECRET = os.environ["ADSEFID_WEBHOOK_SECRET"]
213
+
214
+
215
+ @app.post("/webhooks/adsefid")
216
+ def handle_webhook():
217
+ try:
218
+ event = verify_and_parse_webhook(
219
+ raw_body=request.get_data(), # raw bytes, not request.json
220
+ signature_header=request.headers[WebhookHeaders.SIGNATURE],
221
+ timestamp_header=request.headers[WebhookHeaders.TIMESTAMP],
222
+ secret=WEBHOOK_SECRET,
223
+ )
224
+ except AdsefidWebhookVerificationError as e:
225
+ return {"error": str(e)}, 400
226
+
227
+ # Only event types this webhook endpoint is subscribed to (in your adsefid.com panel) ever
228
+ # arrive here — an endpoint subscribed to just "receive" never sees a "status" event.
229
+ match event.type:
230
+ case WebhookEventType.RECEIVE:
231
+ for item in event.data:
232
+ print("inbound SMS from", item.sender, ":", item.message)
233
+ case WebhookEventType.STATUS:
234
+ for item in event.data:
235
+ print("SMS", item.id, "->", item.status_delivery)
236
+ case WebhookEventType.MESSENGER_STATUS:
237
+ for item in event.data:
238
+ print("messenger message", item.id, "->", item.status_delivery)
239
+
240
+ return {"ok": True}, 200
241
+ ```
242
+
243
+ `WebhookHeaders` exposes every header name as a constant so you never have to type
244
+ `"X-Atlas-Webhook-Signature"` yourself; `WebhookEventType` does the same for the `type` values.
245
+
246
+ Return a `2xx` quickly and process asynchronously where possible — the platform treats any `2xx` as delivered and otherwise retries with backoff (`5s, 60s, 120s, 360s, 600s, 900s`) until it gives up. Handlers should be idempotent using `WebhookHeaders.ID` plus each event item's own id.
247
+
248
+ ## Versioning
249
+
250
+ This SDK follows Semantic Versioning independently of the API documentation.
251
+
252
+ - SDK version: **`0.1.0`** (`version` in `pyproject.toml`; `adsefid.__version__` reads package metadata)
253
+ - Verified API documentation: **`v1.11.0`**
254
+
255
+ SDK releases use `v<SDK_VERSION>` tags. The two version numbers move independently.
256
+
257
+ ## Development
258
+
259
+ ```bash
260
+ make deps # pip install -e ".[dev]" (ruff, mypy, build)
261
+ make fmt # ruff format .
262
+ make lint # ruff check . && ruff format --check . && mypy src/adsefid
263
+ make build # python -m build
264
+ ```
265
+
266
+ ## License
267
+
268
+ Proprietary — All rights reserved.