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.
- adsefid-0.1.0/.gitattributes +1 -0
- adsefid-0.1.0/.github/dependabot.yml +12 -0
- adsefid-0.1.0/.github/workflows/ci.yml +29 -0
- adsefid-0.1.0/.gitignore +23 -0
- adsefid-0.1.0/AGENTS.md +89 -0
- adsefid-0.1.0/CLAUDE.md +89 -0
- adsefid-0.1.0/Makefile +9 -0
- adsefid-0.1.0/PKG-INFO +268 -0
- adsefid-0.1.0/README.md +242 -0
- adsefid-0.1.0/pyproject.toml +50 -0
- adsefid-0.1.0/src/adsefid/__init__.py +65 -0
- adsefid-0.1.0/src/adsefid/_base.py +108 -0
- adsefid-0.1.0/src/adsefid/_serialization.py +112 -0
- adsefid-0.1.0/src/adsefid/async_client.py +90 -0
- adsefid-0.1.0/src/adsefid/client.py +89 -0
- adsefid-0.1.0/src/adsefid/config.py +13 -0
- adsefid-0.1.0/src/adsefid/enums.py +130 -0
- adsefid-0.1.0/src/adsefid/exceptions.py +60 -0
- adsefid-0.1.0/src/adsefid/models/__init__.py +0 -0
- adsefid-0.1.0/src/adsefid/models/common.py +102 -0
- adsefid-0.1.0/src/adsefid/models/messenger.py +356 -0
- adsefid-0.1.0/src/adsefid/models/sms.py +384 -0
- adsefid-0.1.0/src/adsefid/models/user.py +138 -0
- adsefid-0.1.0/src/adsefid/py.typed +0 -0
- adsefid-0.1.0/src/adsefid/resources/__init__.py +0 -0
- adsefid-0.1.0/src/adsefid/resources/messenger.py +266 -0
- adsefid-0.1.0/src/adsefid/resources/sms.py +244 -0
- adsefid-0.1.0/src/adsefid/resources/user.py +107 -0
- adsefid-0.1.0/src/adsefid/webhooks/__init__.py +22 -0
- adsefid-0.1.0/src/adsefid/webhooks/events.py +122 -0
- adsefid-0.1.0/src/adsefid/webhooks/headers.py +23 -0
- adsefid-0.1.0/src/adsefid/webhooks/verifier.py +92 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -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
|
adsefid-0.1.0/.gitignore
ADDED
|
@@ -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
|
adsefid-0.1.0/AGENTS.md
ADDED
|
@@ -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/CLAUDE.md
ADDED
|
@@ -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
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
|
+
[](https://github.com/adsefid/sdk-python/actions/workflows/ci.yml)
|
|
30
|
+
[](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.
|