From 7a7d096420d6376b3a0b26331258fe1e9c8e1990 Mon Sep 17 00:00:00 2001 From: Miradil Zeynalli Date: Thu, 20 Aug 2026 03:15:17 +0200 Subject: [PATCH] Added info about ecustoms --- CLAUDE.md | 161 ++++++++++++++++++++ README.md | 19 +-- docs/az/docs/index.md | 16 +- docs/az/docs/integrations/ecustoms/index.md | 37 +++++ docs/az/mkdocs.yml | 1 + packages/core/CHANGELOG.md | 8 + packages/core/pyproject.toml | 2 +- packages/core/src/integrify/utils.py | 8 +- 8 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/az/docs/integrations/ecustoms/index.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7738d0b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,161 @@ +# Integrify — project guide + +Integrify is a family of Python libraries that wrap Azerbaijani service APIs +(payments, SMS, POS, customs) behind one small, consistent client abstraction. +This repo is a **uv workspace monorepo**: every integration is its own +distribution under `packages/*`, published independently to PyPI, all sharing +the `integrify` PEP 420 namespace. + +## Layout + +``` +pyproject.toml # umbrella `integrify` dist + workspace + tool config (ruff, ty, pytest, coverage) +justfile # every dev command lives here +scripts/run_tests.py # runs each package's suite in its own pytest process +docs/{az,en}/ # mkdocs (zensical) sites; ALL integration docs live here, not in packages/* +packages/core/ # integrify-core: APIClient, APIPayloadHandler, APIExecutor, schemas +packages// # one distribution per integration +``` + +Each integration package looks like this — **add every one of these files when +creating a new integration**: + +``` +packages// +├── CHANGELOG.md # Keep a Changelog format +├── CITATION.cff +├── LICENSE # copy from a sibling package +├── README.md # badges, request table, examples, AUTO-UPDATE SECTION table at the end +├── py.typed +├── pyproject.toml # name = "integrify-", depends on integrify-core +├── src/integrify// +│ ├── __init__.py # docstring with the official doc link; exports Client, AsyncClient, ClientClass, VERSION +│ ├── client.py # ClientClass + module-level Client / AsyncClient singletons +│ ├── env.py # VERSION, env vars, `class API(str, Enum)` of endpoints, __all__ +│ ├── handlers.py # one APIPayloadHandler subclass per endpoint +│ ├── py.typed +│ └── schemas/{__init__.py,request.py,response.py,enums.py,utils.py} +└── tests/{__init__.py,conftest.py,mocks.py,test_*.py} +``` + +Documentation pages do **not** live inside the package. They go under the root +docs site: + +``` +docs/az/docs/integrations// +├── index.md # official-doc link, request table, flow, notes +├── env.md # env-var table + .env template +└── api-reference/ # mkdocstrings stubs: client.md, request.md, response.md, enums.md +``` + +Then wire it up in three places: + +1. `pyproject.toml` — `[project.optional-dependencies]` extra, the `all` list, and `[tool.uv.sources]`. +2. `docs/az/mkdocs.yml` — the mkdocstrings `paths` list **and** the `nav` tree. +3. `docs/az/partial.yml` — the matching nav fragment. + +(English pages, where they exist, mirror this under `docs/en/`.) + +## Commands + +Always go through `just` (it wraps `uv run --no-sync`): + +``` +just setup # uv sync --all-packages + install pre-commit hooks +just format # ruff check --fix-only + ruff format +just lint # ruff check + ruff format --check +just type-check # ty check packages +just test # scripts/run_tests.py (per-package pytest processes, with coverage) +just coverage # combine + report + html +just docs # zensical build -f docs/az/mkdocs.yml --strict +just secure # bandit +just all # format + lint + test + docs +``` + +Tests must run per-package: every package ships a top-level `tests` package, so +collecting them under one root causes `ImportPathMismatchError`. Use +`just test` or `pytest packages//tests` from inside that package. + +Coverage gate is `fail_under = 95`. + +## The client abstraction (packages/core) + +- `APIClient` holds a `base_url`, a `urls` map and a `handlers` map. Endpoints are + registered in `__init__` with `add_url(route_name, url, verb)` + + `add_handler(route_name, HandlerClass)`. There are **no explicit request methods + at runtime** — `APIClient.__getattr__` looks the route name up and builds the + call. Public typing/docs come from `@overload` stubs inside `if TYPE_CHECKING:`. +- `APIPayloadHandler` owns one endpoint's payload. Declare `req_model`, + `resp_model` (and `dry`) as **class attributes**; override + `pre_handle_payload` / `handle_payload` / `post_handle_payload` / `headers` / + `req_args` / `handle_response` as needed. Handlers are **stateless** — never + stash per-request state on `self`, they are shared across concurrent calls. +- `PayloadBaseModel.from_args` maps positional args onto model fields **in field + declaration order**, so a client method's parameter order must match its + request schema's field order. +- `URL_PARAM_FIELDS` (a `ClassVar[set[str]]` on the request schema) marks fields + that are interpolated into the URL instead of the body; the endpoint string in + `env.API` uses those exact snake_case names as `{placeholders}`. +- `APIResponse[T]` wraps the httpx response: `.ok`, `.status_code`, `.headers`, + `.body` (validated as `T`). `T` may be a `BaseModel`, a `dict`, or a `list[...]`. +- `dry=True` on a client returns a `DryResponse` dict (`url`, `verb`, `headers`, + `data`, `request_args`) instead of sending — the cheapest way to unit-test + payload construction. +- Every integration exposes both a sync and an async singleton + (`XClient` / `XAsyncClient`), typed via `Generic[_Mode]` with `_Sync`/`_Async` + markers and paired `@overload`s so both share one docstring. +- `_build_request_lambda` is the subclass hook for injecting per-request headers + (see `clopos` for auth tokens). + +## Conventions + +- **Language**: docstrings, comments and docs are written in **Azerbaijani**; + CHANGELOG entries and this file are in English. Keep it that way. +- **Style**: ruff, line length 100, **single quotes**, target py310. `just format` + before committing; pre-commit enforces it. +- Every public class, function and pydantic field gets a docstring — the docs + site is generated from them via mkdocstrings + griffe-pydantic. +- Client method docstrings follow a fixed shape: one-line summary, `**Endpoint:**`, + an `Example:` block, `**Cavab formatı**:` with a mkdocstrings cross-reference, + a prose note on the flow, then `Args:`. +- **Ordering**: endpoints and everything derived from them — the `env.API` enum, + `add_url`/`add_handler` calls, the `@overload` stubs, handler classes, request + schemas, the docs/README request tables, the `members:` list and the + api-reference stubs — follow the order the endpoints appear in the **official + documentation**, never alphabetically. Where the upstream spec lists its schemas + alphabetically, ignore that ordering and place each schema by the endpoint that + uses it (nested objects just before the model that references them). +- Schemas: use an alias generator (`to_camel` / `to_pascal`) in + `schemas/utils.py:BaseSchema` when the API is internally consistent; use + **explicit per-field aliases** when it is not. +- Response schemas should keep enum-ish fields as `str` and expose the enum + separately for reference, so a newly added upstream value can't break validation. +- Optional arguments use the `UNSET` sentinel (`Unsettable[T] = _UNSET`), not + `None`, so "not passed" is distinguishable from "explicitly null". +- Env vars are read once in `env.py`, warn (don't raise) when missing, and are + documented in `docs/.../env.md` as a table plus a `.env` template. + +## Releasing + +Publishing is tag-driven: `-` (e.g. `epoint-1.3.0`, +`core-1.2.1`, `integrify-3.0.0` for the umbrella). The workflow verifies the tag +is an ancestor of `main` and that the version matches that package's +`pyproject.toml`. So: bump `version` in the package's `pyproject.toml`, add a +CHANGELOG entry with its compare link, then tag. + +Changing `packages/core` means a core release plus a dependency floor bump in +any package that relies on the new behaviour. + +## Private integrations + +Not every integration lives here. `integrify-ecustoms` (State Customs Committee, +Carriers V4) is maintained in a **private** sibling repository, +`integrify-sdk/integrify-ecustoms`, because that API is provided under a carrier +contract. It contributes `integrify.ecustoms` to the same PEP 420 namespace and +follows these same conventions, but is installed from git rather than PyPI. + +This repo keeps only a stub page at +`docs/az/docs/integrations/ecustoms/index.md`. Never add mkdocstrings (`:::`) +directives for it here — that would render the exact API surface the private repo +exists to protect. For the same reason `packages/ecustoms/` must not reappear +under `packages/*`. diff --git a/README.md b/README.md index f3cea66..8964d90 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,13 @@ For migration steps from older per-integration repositories, see [MIGRATION.md]( > > Even though all requests are written according to official documentation, Integrify is unofficial library for these integrations -| Service | Core requests/ Əsas sorğular | All requests/Bütün sorğular | Documentation | Tested in production/Real mühitdə test | Lead developer/Əsas Developer | -| ----------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | -| [EPoint](https://github.com/Integrify-SDK/integrify-epoint-python) | ✅ | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Full](https://integrify.mmzeynalli.dev/integrations/epoint/) | ✅ | [Miradil Zeynallı](https://github.com/mmzeynalli) | -| [KapitalBank](https://github.com/Integrify-SDK/integrify-kapitalbank-python) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/integrations/kapitalbank/) | ✅ | [Zaman Kazımov](https://github.com/kazimovzaman2) | -| [LSIM](https://github.com/Integrify-SDK/integrify-lsim-python) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/integrations/lsim/) | ✅ | [Miradil Zeynallı](https://github.com/mmzeynalli) | -| [Posta Guvercini](https://github.com/Integrify-SDK/integrify-postaguvercini-python) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/integrations/posta-guvercini/) | ✅ | [Zaman Kazımov](https://github.com/kazimovzaman2) | -| [Azericard](https://github.com/Integrify-SDK/integrify-azericard-python) | ✅ | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Full](https://integrify.mmzeynalli.dev/integrations/azericard/) | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Miradil Zeynallı](https://github.com/mmzeynalli) | -| [Clopos](https://github.com/Integrify-SDK/integrify-clopos-python) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/en/integrations/clopos/) | ![loading](https://raw.githubusercontent.com/mmzeynalli/integrify/main/docs/az/docs/assets/spinner-solid.svg) | [Miradil Zeynallı](https://github.com/mmzeynalli) | -| [Payriff](https://github.com/Integrify-SDK/integrify-payriff-python) | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | ![loading](https://raw.githubusercontent.com/Integrify-SDK/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Vahid Həsənzadə](https://github.com/vahidzhe) | +| Service | Core requests/ Əsas sorğular | All requests/Bütün sorğular | Documentation | Tested in production/Real mühitdə test | Lead developer/Əsas Developer | +| ------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| [EPoint](https://github.com/integrify-sdk/integrify-python/tree/main/packages/epoint) | ✅ | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Full](https://integrify.mmzeynalli.dev/integrations/epoint) | ✅ | [Miradil Zeynallı](https://github.com/mmzeynalli) | +| [KapitalBank](https://github.com/integrify-sdk/integrify-python/tree/main/packages/kapitalbank) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/integrations/kapitalbank) | ✅ | [Zaman Kazımov](https://github.com/kazimovzaman2) | +| [LSIM](https://github.com/integrify-sdk/integrify-python/tree/main/packages/lsim) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/integrations/lsim) | ✅ | [Miradil Zeynallı](https://github.com/mmzeynalli) | +| [Posta Guvercini](https://github.com/integrify-sdk/integrify-python/tree/main/packages/postaguvercini) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/integrations/posta-guvercini) | ✅ | [Zaman Kazımov](https://github.com/kazimovzaman2) | +| [Azericard](https://github.com/integrify-sdk/integrify-python/tree/main/packages/azericard) | ✅ | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Full](https://integrify.mmzeynalli.dev/integrations/azericard) | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Miradil Zeynallı](https://github.com/mmzeynalli) | +| [Clopos](https://github.com/integrify-sdk/integrify-python/tree/main/packages/clopos) | ✅ | ✅ | [Full](https://integrify.mmzeynalli.dev/en/integrations/clopos) | ![loading](https://raw.githubusercontent.com/mmzeynalli/integrify/main/docs/az/docs/assets/spinner-solid.svg) | [Miradil Zeynallı](https://github.com/mmzeynalli) | +| [ECustoms (məxfi)](https://github.com/integrify-sdk/integrify-ecustoms) | ✅ | ✅ | [Məxfi](https://integrify.mmzeynalli.dev/integrations/ecustoms/) | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Miradil Zeynallı](https://github.com/mmzeynalli) | +| [Payriff](https://github.com/integrify-sdk/integrify-python/tree/main/packages/payriff) | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | ![loading](https://raw.githubusercontent.com/integrify-sdk/integrify-docs-python/main/docs/az/docs/assets/spinner-solid.svg) | [Vahid Həsənzadə](https://github.com/vahidzhe) | diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md index d095b95..75d5c82 100644 --- a/docs/az/docs/index.md +++ b/docs/az/docs/index.md @@ -14,13 +14,15 @@ Bütün sorğuların həm **sinxron**, həm də **asinxron** versiyası var və ## İnteqrasiyalar -| İnteqrasiya | Növ | Sənəd | -| :--- | :--- | :--- | -| EPoint | Ödəniş | [Keçid](integrations/epoint/index.md) | -| Kapital Bank | Ödəniş | [Keçid](integrations/kapitalbank/index.md) | -| Azericard | Ödəniş | [Keçid](integrations/azericard/index.md) | -| LSIM | SMS | [Keçid](integrations/lsim/index.md) | -| Posta Güvərçini | SMS | [Keçid](integrations/posta-guvercini/index.md) | +| İnteqrasiya | Növ | Sənəd | +| :-------------- | :------------------------- | :--------------------------------------------------------------------- | +| EPoint | Ödəniş | [Keçid](integrations/epoint/index.md) | +| Kapital Bank | Ödəniş | [Keçid](integrations/kapitalbank/index.md) | +| Azericard | Ödəniş | [Keçid](integrations/azericard/index.md) | +| LSIM | SMS | [Keçid](integrations/lsim/index.md) | +| Posta Güvərçini | SMS | [Keçid](integrations/posta-guvercini/index.md) | +| Clopos | Restoran idarəetməsi (POS) | [Keçid (EN)](https://integrify.mmzeynalli.dev/en/integrations/clopos/) | +| ECustoms | Logistika (məxfi) | [Keçid](integrations/ecustoms/index.md) | > Clopos (POS) inteqrasiyasının sənədi hələlik yalnız **[İngiliscə](https://integrify.mmzeynalli.dev/en/)** mövcuddur. diff --git a/docs/az/docs/integrations/ecustoms/index.md b/docs/az/docs/integrations/ecustoms/index.md new file mode 100644 index 0000000..7e50f8e --- /dev/null +++ b/docs/az/docs/integrations/ecustoms/index.md @@ -0,0 +1,37 @@ +# E-Customs + +Azərbaycan Respublikası Dövlət Gömrük Komitəsinin (DGK) daşıyıcı şirkətlər üçün +nəzərdə tutulmuş `Carriers V4` API-si üçün Integrify inteqrasiyası. + +???+ warning "Bu inteqrasiya məxfidir" + `integrify-ecustoms` paketi **ictimai deyil** və PyPI-a dərc olunmur. DGK-nın + `Carriers V4` API-si daşıyıcı şirkətlərlə bağlanan müqavilə əsasında verildiyi + üçün, paketin kodu və sənədi yalnız girişi olan komandalar üçün açıqdır. + +## Nə edir? { #what-it-does } + +Daşıyıcı şirkətin anbarına daxil olan bağlamaların DGK-da qeydiyyatını, vətəndaş +bəyannamələrinin alınmasını və gömrük əməliyyatlarının izlənməsini avtomatlaşdırır: +bağlamanın bildirilməsi → bəyannamənin alınması və təsdiqlənməsi → qutuya yığılma → +depeş → gömrük əməliyyatlarının izlənməsi. + +Digər inteqrasiyalar kimi eyni `integrify` namespace-ini paylaşır: + +```python +from integrify.ecustoms import ECustomsClient # məxfi paket +``` + +## Giriş { #access } + +Paketə giriş üçün DGK ilə daşıyıcı müqaviləniz olmalıdır. Müraciət üçün: +[Integrify-SDK](https://github.com/integrify-sdk) komandası ilə əlaqə saxlayın. + +Giriş verildikdən sonra paket birbaşa məxfi repodan quraşdırılır: + +```toml +[tool.uv.sources] +integrify-ecustoms = { git = "ssh://git@github.com/integrify-sdk/integrify-ecustoms.git", tag = "v1.0.0" } +``` + +Sorğuların tam siyahısı, mühit dəyişənləri və API referansı həmin reponun +daxili sənəd saytındadır. diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 2afb800..ac0a3fd 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -163,3 +163,4 @@ nav: - integrations/posta-guvercini/api-reference/client.md - Schemas: - Response: integrations/posta-guvercini/api-reference/response.md + - E-Customs (məxfi): integrations/ecustoms/index.md diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 557aaf6..bc29263 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to `integrify-core` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project follows [Semantic Versioning](https://semver.org/). +## [1.3.0] - 2026-08-17 + +### Changed + +- Widened the `_ResponseT` bound to `BaseModel | dict | list`, so integrations whose + endpoints return a JSON array at the root can type their responses as + `APIResponse[list[SomeSchema]]`. + ## [1.2.0] - 2026-08-11 ### Added diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index 1c3f217..53d932c 100644 --- a/packages/core/pyproject.toml +++ b/packages/core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "integrify-core" -version = "1.2.0" +version = "1.3.0" description = "Integrify API inteqrasiyalarını rahatlaşdıran bünovrə (core) kitabxanadır." authors = [{ name = "mmzeynalli", email = "miradil.zeynalli@gmail.com" }] requires-python = ">=3.10" diff --git a/packages/core/src/integrify/utils.py b/packages/core/src/integrify/utils.py index 1eecc58..141e494 100644 --- a/packages/core/src/integrify/utils.py +++ b/packages/core/src/integrify/utils.py @@ -3,8 +3,12 @@ from pydantic import BaseModel, Field -_ResponseT = TypeVar('_ResponseT', bound=BaseModel | dict) -"""Dynamic response type.""" +_ResponseT = TypeVar('_ResponseT', bound=BaseModel | dict | list) +"""Dynamic response type. + +Bəzi API-lər (məs., E-Customs) kök səviyyəsində JSON array qaytarır, ona görə +`list` də bound-a daxildir (`APIResponse[list[SomeSchema]]`). +""" T = TypeVar('T')