schedule-bit 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,7 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .live-sandbox.json
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ The first release.
6
+
7
+ - One method per API operation (sync `Client`, async `AsyncClient`), typed with
8
+ models generated from the OpenAPI contract.
9
+ - 429 and 503 are retried, and network failures and other 5xx where it is safe,
10
+ with jittered back-off that honours `Retry-After` and `RateLimit-Reset`.
11
+ - The writes that take an `Idempotency-Key` get one automatically, the same
12
+ across a call's retries.
13
+ - `RateLimit-*` headers are read on every answer; an optional client-side pacer
14
+ keeps you under a rate.
15
+ - Typed errors: `Refusal` (409), `LocalTimeError` (422), `RateLimited`,
16
+ `NetworkError`, `TimeoutError`. `try_claim` returns a refusal instead of raising.
17
+ - `Slots` for hex masks; `local_day`, `local_hour` and `local_slot` for local time.
18
+ - `watch()` is a resuming WebSocket iterator (`schedule-bit[watch]`); `assist()`
19
+ streams the Director's events.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ontology.io
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.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.5
2
+ Name: schedule-bit
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the schedule-bit API: read and write time as bitmasks, with retries, rate limits and live watches handled.
5
+ Project-URL: Homepage, https://schedule-bit.io
6
+ Project-URL: Documentation, https://api.schedule-bit.io/v1/docs
7
+ Project-URL: Source, https://github.com/ontology-io/schedule-bit/tree/main/sdks/python
8
+ Project-URL: Changelog, https://github.com/ontology-io/schedule-bit/blob/main/sdks/python/CHANGELOG.md
9
+ Author: Ontology.io
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,bitmask,booking,calendar,schedule,scheduling,sdk
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx<1,>=0.27
24
+ Requires-Dist: pydantic<3,>=2.7
25
+ Provides-Extra: watch
26
+ Requires-Dist: websockets<16,>=13; extra == 'watch'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # schedule-bit for Python
30
+
31
+ The [schedule-bit](https://schedule-bit.io) API from Python. It keeps time as bitmasks, so claiming a resource never double-books it. This SDK covers every operation, sync and async. Retries, rate limits, idempotency and live updates are handled for you.
32
+
33
+ ```sh
34
+ pip install schedule-bit # or: uv add schedule-bit
35
+ pip install "schedule-bit[watch]" # adds live watching over WebSocket
36
+ ```
37
+
38
+ Python 3.10 or newer.
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from schedule_bit import Client, Refusal, Slots
44
+
45
+ with Client() as api: # key from SCHEDULE_BIT_API_KEY
46
+ api.create_world(name="room-1", preset="temporal-hour")
47
+ api.declare_occupier("room-1", name="omar")
48
+ api.declare_occupier("room-1", name="sara")
49
+
50
+ receipt = api.claim("room-1", who="omar", date="2026-11-12", slots=Slots.range(9, 11))
51
+ print(receipt.slots) # [9, 10]
52
+
53
+ outcome = api.try_claim("room-1", who="sara", date="2026-11-12", slots=[10])
54
+ if isinstance(outcome, Refusal):
55
+ print(outcome.by, outcome.detail) # held, "slot 10 is held by omar"
56
+ ```
57
+
58
+ Async is the same, with `await`:
59
+
60
+ ```python
61
+ from schedule_bit import AsyncClient
62
+
63
+ async with AsyncClient() as api:
64
+ free = await api.offered("room-1", date="2026-11-12")
65
+ print(free.slots)
66
+ ```
67
+
68
+ Every operation in the [API reference](https://api.schedule-bit.io/v1/docs) is a method here in `snake_case`, for example `explainClaim` is `explain_claim`. Body fields are keyword arguments, in Python or API spelling. Answers are typed models in `schedule_bit.models`.
69
+
70
+ ## What the client does for you
71
+
72
+ | | |
73
+ |---|---|
74
+ | **Retries** | 429 and 503 always, and network failures or other 5xx when the call is safe to repeat (a read, `explain_claim`, or a write with an idempotency key). Waits use jittered back-off that honours `Retry-After` and `RateLimit-Reset`. The default is 4 retries; set `max_retries=0` to turn them off. |
75
+ | **Idempotency** | Writes that accept an `Idempotency-Key` get one, the same across that call's retries, so a retried claim never claims twice. To pass your own, use `options=RequestOptions(idempotency_key=…)`. |
76
+ | **Rate limits** | `api.rate_limit` holds the latest `RateLimit-*` headers, `on_rate_limit=` is called with each one, and `rate_limit_per_minute=` paces requests on the client side. |
77
+ | **Timeouts** | 30 s per attempt, which you can change per client (`timeout=`) or per call (`RequestOptions(timeout=…)`). A sync call can be cancelled with `RequestOptions(cancel=threading.Event())`; an async call is cancelled by cancelling its task. |
78
+ | **Errors** | `Refusal` (409, the engine said no, `.by` names why), `LocalTimeError` (422, with `.candidates`), `RateLimited`, `ApiError` for any other problem, and `NetworkError` / `TimeoutError` with the last cause attached. |
79
+ | **Secrets** | Keys never appear in errors, reprs or logs. |
80
+
81
+ ## Masks and local time
82
+
83
+ ```python
84
+ from datetime import datetime
85
+ from zoneinfo import ZoneInfo
86
+ from schedule_bit import Slots, local_slot
87
+
88
+ Slots.from_hex("0x600").list() # [9, 10]
89
+ (Slots.range(9, 12) - [10]).hex # "0xa00"
90
+
91
+ at = datetime(2026, 10, 25, 1, 30, tzinfo=ZoneInfo("Europe/London"), fold=1)
92
+ api.claim("room-1", who="omar", **local_slot(at)) # the second 01:30 that night
93
+ ```
94
+
95
+ At a clock change, Python's `fold` says which of a repeated time you mean, and `local_slot` sends it as `occurrence`. Without it, the API answers with a `LocalTimeError` that lists the candidates.
96
+
97
+ ## Watching a World
98
+
99
+ ```python
100
+ with api.watch("room-1") as frames:
101
+ for frame in frames:
102
+ print(frame.type) # hello, claim.made, claim.released, …
103
+ ```
104
+
105
+ If the connection drops, the watch reconnects with the same back-off. It catches up from the journal, so no commit is missed, and it yields each event once.
106
+
107
+ ## Development
108
+
109
+ ```sh
110
+ uv sync
111
+ uv run python scripts/generate.py # after the contract changes
112
+ uv run pytest # unit tests (a fake server, no sleeping)
113
+ SCHEDULE_BIT_LIVE=1 uv run pytest -m live # the shared scenarios against the dev API
114
+ uv run ruff check && uv run mypy
115
+ ```
116
+
117
+ `src/schedule_bit/_generated/` is generated from `sdks/spec/openapi.json`, so don't edit it by hand. A unit test fails when it is stale. The behaviour every SDK shares is in [`sdks/POLICY.md`](../POLICY.md).
118
+
119
+ MIT licensed.
@@ -0,0 +1,91 @@
1
+ # schedule-bit for Python
2
+
3
+ The [schedule-bit](https://schedule-bit.io) API from Python. It keeps time as bitmasks, so claiming a resource never double-books it. This SDK covers every operation, sync and async. Retries, rate limits, idempotency and live updates are handled for you.
4
+
5
+ ```sh
6
+ pip install schedule-bit # or: uv add schedule-bit
7
+ pip install "schedule-bit[watch]" # adds live watching over WebSocket
8
+ ```
9
+
10
+ Python 3.10 or newer.
11
+
12
+ ## Quick start
13
+
14
+ ```python
15
+ from schedule_bit import Client, Refusal, Slots
16
+
17
+ with Client() as api: # key from SCHEDULE_BIT_API_KEY
18
+ api.create_world(name="room-1", preset="temporal-hour")
19
+ api.declare_occupier("room-1", name="omar")
20
+ api.declare_occupier("room-1", name="sara")
21
+
22
+ receipt = api.claim("room-1", who="omar", date="2026-11-12", slots=Slots.range(9, 11))
23
+ print(receipt.slots) # [9, 10]
24
+
25
+ outcome = api.try_claim("room-1", who="sara", date="2026-11-12", slots=[10])
26
+ if isinstance(outcome, Refusal):
27
+ print(outcome.by, outcome.detail) # held, "slot 10 is held by omar"
28
+ ```
29
+
30
+ Async is the same, with `await`:
31
+
32
+ ```python
33
+ from schedule_bit import AsyncClient
34
+
35
+ async with AsyncClient() as api:
36
+ free = await api.offered("room-1", date="2026-11-12")
37
+ print(free.slots)
38
+ ```
39
+
40
+ Every operation in the [API reference](https://api.schedule-bit.io/v1/docs) is a method here in `snake_case`, for example `explainClaim` is `explain_claim`. Body fields are keyword arguments, in Python or API spelling. Answers are typed models in `schedule_bit.models`.
41
+
42
+ ## What the client does for you
43
+
44
+ | | |
45
+ |---|---|
46
+ | **Retries** | 429 and 503 always, and network failures or other 5xx when the call is safe to repeat (a read, `explain_claim`, or a write with an idempotency key). Waits use jittered back-off that honours `Retry-After` and `RateLimit-Reset`. The default is 4 retries; set `max_retries=0` to turn them off. |
47
+ | **Idempotency** | Writes that accept an `Idempotency-Key` get one, the same across that call's retries, so a retried claim never claims twice. To pass your own, use `options=RequestOptions(idempotency_key=…)`. |
48
+ | **Rate limits** | `api.rate_limit` holds the latest `RateLimit-*` headers, `on_rate_limit=` is called with each one, and `rate_limit_per_minute=` paces requests on the client side. |
49
+ | **Timeouts** | 30 s per attempt, which you can change per client (`timeout=`) or per call (`RequestOptions(timeout=…)`). A sync call can be cancelled with `RequestOptions(cancel=threading.Event())`; an async call is cancelled by cancelling its task. |
50
+ | **Errors** | `Refusal` (409, the engine said no, `.by` names why), `LocalTimeError` (422, with `.candidates`), `RateLimited`, `ApiError` for any other problem, and `NetworkError` / `TimeoutError` with the last cause attached. |
51
+ | **Secrets** | Keys never appear in errors, reprs or logs. |
52
+
53
+ ## Masks and local time
54
+
55
+ ```python
56
+ from datetime import datetime
57
+ from zoneinfo import ZoneInfo
58
+ from schedule_bit import Slots, local_slot
59
+
60
+ Slots.from_hex("0x600").list() # [9, 10]
61
+ (Slots.range(9, 12) - [10]).hex # "0xa00"
62
+
63
+ at = datetime(2026, 10, 25, 1, 30, tzinfo=ZoneInfo("Europe/London"), fold=1)
64
+ api.claim("room-1", who="omar", **local_slot(at)) # the second 01:30 that night
65
+ ```
66
+
67
+ At a clock change, Python's `fold` says which of a repeated time you mean, and `local_slot` sends it as `occurrence`. Without it, the API answers with a `LocalTimeError` that lists the candidates.
68
+
69
+ ## Watching a World
70
+
71
+ ```python
72
+ with api.watch("room-1") as frames:
73
+ for frame in frames:
74
+ print(frame.type) # hello, claim.made, claim.released, …
75
+ ```
76
+
77
+ If the connection drops, the watch reconnects with the same back-off. It catches up from the journal, so no commit is missed, and it yields each event once.
78
+
79
+ ## Development
80
+
81
+ ```sh
82
+ uv sync
83
+ uv run python scripts/generate.py # after the contract changes
84
+ uv run pytest # unit tests (a fake server, no sleeping)
85
+ SCHEDULE_BIT_LIVE=1 uv run pytest -m live # the shared scenarios against the dev API
86
+ uv run ruff check && uv run mypy
87
+ ```
88
+
89
+ `src/schedule_bit/_generated/` is generated from `sdks/spec/openapi.json`, so don't edit it by hand. A unit test fails when it is stale. The behaviour every SDK shares is in [`sdks/POLICY.md`](../POLICY.md).
90
+
91
+ MIT licensed.
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "schedule-bit"
3
+ version = "0.1.0"
4
+ description = "Python SDK for the schedule-bit API: read and write time as bitmasks, with retries, rate limits and live watches handled."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Ontology.io" }]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "httpx>=0.27,<1",
12
+ "pydantic>=2.7,<3",
13
+ ]
14
+ keywords = ["schedule", "scheduling", "bitmask", "calendar", "booking", "api", "sdk"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ watch = ["websockets>=13,<16"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://schedule-bit.io"
32
+ Documentation = "https://api.schedule-bit.io/v1/docs"
33
+ Source = "https://github.com/ontology-io/schedule-bit/tree/main/sdks/python"
34
+ Changelog = "https://github.com/ontology-io/schedule-bit/blob/main/sdks/python/CHANGELOG.md"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "pytest>=8",
39
+ "pytest-asyncio>=0.24",
40
+ "ruff>=0.6",
41
+ "mypy>=1.11",
42
+ "websockets>=13,<16",
43
+ "datamodel-code-generator==0.83.0",
44
+ ]
45
+
46
+ [build-system]
47
+ requires = ["hatchling>=1.27"]
48
+ build-backend = "hatchling.build"
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/schedule_bit"]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ include = ["src", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ asyncio_mode = "auto"
59
+ asyncio_default_fixture_loop_scope = "function"
60
+ markers = ["live: runs against the deployed dev API (needs an operator key)"]
61
+ addopts = "-m 'not live'"
62
+
63
+ [tool.ruff]
64
+ line-length = 120
65
+ target-version = "py310"
66
+ extend-exclude = ["src/schedule_bit/_generated"]
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
70
+ ignore = ["RUF001", "RUF002", "RUF003"]
71
+
72
+ [tool.mypy]
73
+ python_version = "3.10"
74
+ strict = true
75
+ plugins = ["pydantic.mypy"]
76
+ files = ["src/schedule_bit"]
77
+ exclude = ["src/schedule_bit/_generated/"]
78
+
79
+ [[tool.mypy.overrides]]
80
+ module = ["schedule_bit._generated.*"]
81
+ ignore_errors = true
@@ -0,0 +1,57 @@
1
+ """The schedule-bit API from Python.
2
+
3
+ from schedule_bit import Client, Slots
4
+
5
+ with Client() as api: # key from SCHEDULE_BIT_API_KEY
6
+ api.create_world(name="room-1", preset="temporal-hour")
7
+ api.declare_occupier("room-1", name="omar")
8
+ receipt = api.claim("room-1", who="omar", date="2026-10-15", slots=Slots.range(9, 11))
9
+
10
+ See https://api.schedule-bit.io/v1/docs for every operation; each one is a method
11
+ here, in ``snake_case`` (``explainClaim`` is ``explain_claim``).
12
+ """
13
+
14
+ from ._client import DEFAULT_BASE_URL, AsyncClient, Client
15
+ from ._generated import models
16
+ from ._options import RequestOptions
17
+ from ._redact import redact
18
+ from ._version import __version__
19
+ from .errors import (
20
+ ApiError,
21
+ Cancelled,
22
+ LocalTimeError,
23
+ NetworkError,
24
+ RateLimited,
25
+ Refusal,
26
+ ScheduleBitError,
27
+ TimeoutError,
28
+ )
29
+ from .local_time import local_day, local_hour, local_slot
30
+ from .masks import Slots
31
+ from .rate_limit import RateLimit, TokenBucket
32
+ from .retry import RetryPolicy
33
+
34
+ __all__ = [
35
+ "DEFAULT_BASE_URL",
36
+ "ApiError",
37
+ "AsyncClient",
38
+ "Cancelled",
39
+ "Client",
40
+ "LocalTimeError",
41
+ "NetworkError",
42
+ "RateLimit",
43
+ "RateLimited",
44
+ "Refusal",
45
+ "RequestOptions",
46
+ "RetryPolicy",
47
+ "ScheduleBitError",
48
+ "Slots",
49
+ "TimeoutError",
50
+ "TokenBucket",
51
+ "__version__",
52
+ "local_day",
53
+ "local_hour",
54
+ "local_slot",
55
+ "models",
56
+ "redact",
57
+ ]