This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
swapi-client is a Python client library for the Serwis Planner API. It supports both synchronous and asynchronous usage. Resources are accessed as attributes of the client (Stripe-style), and query parameters are passed as kwargs — no separate query builder class.
Package Management:
- This project uses
uvfor package management - Install dependencies:
uv sync - Run tests:
uv run python test_filters.py
Building and Publishing:
- Build package:
uv build - Publish to PyPI:
uv publish --token $PYPI_TOKEN(handled by CI/CD)
Python Version:
- Required: Python 3.12+ (specified in
.python-version)
_http.py — low-level HTTP clients
BaseSyncClient— wrapshttpx.Client; sync context manager (with)BaseAsyncClient— wrapshttpx.AsyncClient; async context manager (async with)- Both expose
set_token(),request(),get/post/put/patch/delete() - Error handling raises typed exceptions (see Exceptions section)
_params.py — query parameter builder
- Single function
build_params(**kwargs)converts kwargs → flat URL params dict - Called internally by every resource method; never used directly by end users
client.py — public entry points (~100 lines)
SWApiClient(BaseSyncClient)— sync client; resources attached in__init__AsyncSWApiClient(BaseAsyncClient)— async client; resources attached in__init__- Both expose
me()/await me()→/api/me(current user data)
resources/ — one file per domain, each with Sync* and Async* classes
_base.py—SyncResource/AsyncResourcewith standard CRUD +all()auto-paginationauth.py—login()account.py—AccountResource→.companies+.userscommissions.py—CommissionsResource→.attributes.phases.scope_typesetc.files.py—FilesResource→.directories+upload()+upload_from_urls()kanbans.py,places.pyproducts.py—ProductsResource→.attributes.categories.templates+generate_pdf()serviced_products.py—ServicedProductsResource→.attributes+generate_pdf()users.py—UsersResource+UserProfilesResource
exceptions.py — typed exception hierarchy (see below)
src/swapi_client/
├── __init__.py # Public exports
├── client.py # SWApiClient + AsyncSWApiClient
├── exceptions.py # Exception hierarchy
├── _http.py # BaseSyncClient + BaseAsyncClient
├── _params.py # build_params()
└── resources/
├── __init__.py
├── _base.py # SyncResource + AsyncResource
├── auth.py
├── account.py
├── commissions.py
├── files.py
├── kanbans.py
├── places.py
├── products.py
├── serviced_products.py
└── users.py
from swapi_client import SWApiClient
with SWApiClient("https://api.url") as client:
client.auth.login(client_id="...", auth_token="...", login="...", password="...")
user = client.me()
companies = client.account.companies.list(filter={"name__contains": "STB"}, limit=50)
company = client.account.companies.retrieve(123)
new_co = client.account.companies.create({"name": "New Co"})
updated = client.account.companies.update(123, {"name": "Updated"})
client.account.companies.delete(123)
all_cos = client.account.companies.all(filter={"status": "active"})
phases = client.commissions.phases.list()
products = client.products.list(filter={"attributes.476__hasText": "keyword"})
pdf = client.products.generate_pdf(product_id=123, template_id=1)
uploaded = client.files.upload(files={"file": open("doc.pdf", "rb")})from swapi_client import AsyncSWApiClient
async with AsyncSWApiClient("https://api.url") as client:
await client.auth.login(client_id="...", auth_token="...", login="...", password="...")
user = await client.me()
companies = await client.account.companies.list(filter={"name__contains": "STB"})
all_cos = await client.account.companies.all()
phases = await client.commissions.phases.list()
pdf = await client.products.generate_pdf(product_id=123, template_id=1)All resource methods accept the following kwargs:
| kwarg | URL param | Example |
|---|---|---|
filter={"name__contains": "STB"} |
filter[name][contains]=STB |
operator after __ |
filter={"status": "active"} |
filter[status][eq]=active |
no __ → eq |
filter={"attributes.476__hasText": "x"} |
filter[attributes][476][hasText]=x |
numeric segment → wiele nawiasów |
filter={"commissionPhase.commissionPhaseId": 1} |
filter[commissionPhase.commissionPhaseId][eq]=1 |
bez segmentu numerycznego → dot literalny |
filter_or=[{...}, {...}] |
filterOr[0][...]=... |
list of dicts |
filter_and=[{...}, {...}] |
filterAnd[0][...]=... |
list of dicts |
order={"name": "asc"} |
order[name]=asc |
|
fields=["id", "name"] |
fields=id,name |
|
extra_fields=["address"] |
extra_fields=address |
|
limit=50 |
page[limit]=50 |
|
page=1 |
page[number]=1 |
|
offset=0 |
page[offset]=0 |
|
with_relations=True |
setting[with_relations]=true |
|
lang="pl" |
setting[lang]=pl |
|
for_metadata={"id": 1} |
for[id]=1 |
Operators for isNull / isNotNull: use empty string as value, e.g. filter={"deleted_at__isNull": ""}.
SWException (base)
├── SWHTTPError — any HTTP error (has .status_code, .response_data)
│ ├── SWAuthenticationError — 401
│ ├── SWForbiddenError — 403
│ ├── SWNotFoundError — 404
│ ├── SWValidationError — 422 (has .errors with field-level details)
│ ├── SWRateLimitError — 429
│ └── SWServerError — 5xx
└── SWConnectionError — network / timeout
All exceptions are importable directly from swapi_client:
from swapi_client import SWNotFoundError, SWValidationError, SWConnectionError
try:
client.account.companies.retrieve(999)
except SWNotFoundError:
...
except SWValidationError as e:
print(e.errors)
except SWConnectionError:
...- Create
src/swapi_client/resources/<name>.pywithSync<Name>ResourceandAsync<Name>Resource - Inherit from
SyncResource/AsyncResourceand set_path - Add sub-resources in
__init__if needed - Register on both
SWApiClientandAsyncSWApiClientinclient.py - Export from
resources/__init__.py
- Create client instance with API URL
- Enter context manager (
with/async with) - Call
client.auth.login(client_id, auth_token, login, password)— stores Bearer token automatically - All subsequent requests include the token
Login endpoint — note: no /api/ prefix:
POST /_/security/login
Content-Type: application/json
{"clientId": "...", "authToken": "...", "login": "...", "password": "..."}
Automatic Releases (.github/workflows/release.yml)
- Triggered on push to
master(excluding README/gitignore changes) - Auto-increments patch version in
pyproject.toml - Creates git tag and GitHub release
- Commits include
[skip ci]to prevent recursive triggers
PyPI Publishing (.github/workflows/publish.yml)
- Triggered on GitHub release publication
- Builds package with
uv build - Publishes to PyPI with
uv publish
- Standard: OpenAPI 3.0
- Base path:
/api/... - Data format:
application/json - Authorization required for most endpoints
filter[field][eq]=value
filter[id][gt]=10
filter[name][hasText]=abc
filter[attributes][476][hasText]=text ← nested
filterOr[0][name][contains]=STB
filterAnd[0][status][eq]=active
order[field]=asc
order[field]=desc
fields=id,name,status
extra_fields=address,ph.fullName
page[limit]=50
page[number]=1
page[offset]=0
setting[with_relations]=true
setting[with_cache]=true
setting[limit_to_my_settings]=true
setting[lang]=pl