python-kanka 2.4.1__tar.gz → 2.6.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.
- {python_kanka-2.4.1 → python_kanka-2.6.0}/.bumpversion.cfg +1 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/.github/workflows/publish.yml +2 -2
- {python_kanka-2.4.1 → python_kanka-2.6.0}/CLAUDE.md +41 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/PKG-INFO +1 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/README.md +1 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/api-reference.md +25 -3
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/entities.md +7 -5
- python_kanka-2.6.0/docs/last-sync.md +132 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/pagination.md +8 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/search-and-filtering.md +2 -9
- {python_kanka-2.4.1 → python_kanka-2.6.0}/pyproject.toml +1 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/_version.py +1 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/client.py +76 -3
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/managers.py +31 -3
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_entities_api_integration.py +136 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_client.py +167 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_managers.py +54 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/utils.py +10 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/uv.lock +1 -1
- {python_kanka-2.4.1 → python_kanka-2.6.0}/.github/renovate.json +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/.github/workflows/ci.yml +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/.gitignore +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/.pre-commit-config.yaml +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/CHANGELOG.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/LICENSE +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/MANIFEST.in +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/Makefile +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/PUBLISHING.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/README.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/assets-and-images.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/core-concepts.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/debug-mode.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/entity-types-reference.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/error-handling.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/gallery.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/getting-started.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/known-limitations.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/posts.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/docs/rate-limiting.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/pytest.ini +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/__init__.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/exceptions.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/models/__init__.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/models/base.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/models/common.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/models/entities.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/py.typed +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/src/kanka/types.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/__init__.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/README.md +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/__init__.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/base.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/run_integration_tests.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/setup_test_env.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_calendars_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_calendars_simple.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_characters_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_creatures_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_entity_assets_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_entity_images_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_entity_tags_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_events_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_families_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_gallery_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_journals_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_locations_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_mentions_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_notes_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_organisations_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_posts_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_quests_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_races_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_rate_limiting.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_search_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_tag_colour.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/integration/test_tags_integration.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/mypy.ini +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_client_advanced.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_entity_models.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_exceptions.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_models.py +0 -0
- {python_kanka-2.4.1 → python_kanka-2.6.0}/tests/test_models_advanced.py +0 -0
|
@@ -56,7 +56,7 @@ jobs:
|
|
|
56
56
|
uv build
|
|
57
57
|
|
|
58
58
|
- name: Upload artifacts
|
|
59
|
-
uses: actions/upload-artifact@
|
|
59
|
+
uses: actions/upload-artifact@v6
|
|
60
60
|
with:
|
|
61
61
|
name: dist
|
|
62
62
|
path: dist/
|
|
@@ -71,7 +71,7 @@ jobs:
|
|
|
71
71
|
|
|
72
72
|
steps:
|
|
73
73
|
- name: Download artifacts
|
|
74
|
-
uses: actions/download-artifact@
|
|
74
|
+
uses: actions/download-artifact@v7
|
|
75
75
|
with:
|
|
76
76
|
name: dist
|
|
77
77
|
path: dist/
|
|
@@ -142,6 +142,46 @@ Integration tests are NOT pytest tests - they have custom runners:
|
|
|
142
142
|
- **Available in Kanka API but not yet implemented**: Timeline, Item, Relation, DiceRoll, Conversation, AttributeTemplate, Bookmark, Ability, Map, Inventory
|
|
143
143
|
- **Never existed/removed**: EntityNote, EntityEvent, Attribute, Species
|
|
144
144
|
|
|
145
|
+
## Version Bumping
|
|
146
|
+
|
|
147
|
+
This project uses `bump2version` for version management. The version is tracked in three files:
|
|
148
|
+
- `pyproject.toml` — package version
|
|
149
|
+
- `src/kanka/_version.py` — runtime version
|
|
150
|
+
- `.bumpversion.cfg` — bump2version's own tracking (must stay in sync)
|
|
151
|
+
|
|
152
|
+
**To bump the version:**
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Patch (bug fixes): 2.4.1 → 2.4.2
|
|
156
|
+
uv run bump2version patch
|
|
157
|
+
|
|
158
|
+
# Minor (new features): 2.4.1 → 2.5.0
|
|
159
|
+
uv run bump2version minor
|
|
160
|
+
|
|
161
|
+
# Major (breaking changes): 2.4.1 → 3.0.0
|
|
162
|
+
uv run bump2version major
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This automatically:
|
|
166
|
+
1. Updates the version in `pyproject.toml`, `src/kanka/_version.py`, and `.bumpversion.cfg`
|
|
167
|
+
2. Creates a commit with message `Bump version: X.Y.Z → A.B.C`
|
|
168
|
+
3. Creates a git tag `vA.B.C`
|
|
169
|
+
|
|
170
|
+
**After bumping, always run `uv sync`** to update `uv.lock`, then amend the lock file into the bump commit and retag:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
uv sync
|
|
174
|
+
git add uv.lock
|
|
175
|
+
git commit --amend --no-edit
|
|
176
|
+
git tag -d vA.B.C
|
|
177
|
+
git tag vA.B.C
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Important notes:**
|
|
181
|
+
- The working tree must be clean before running bump2version (use `--allow-dirty` only if `.bumpversion.cfg` is the dirty file due to a sync fix)
|
|
182
|
+
- If `.bumpversion.cfg`'s `current_version` is out of sync with the actual version in `pyproject.toml`, fix it manually before bumping
|
|
183
|
+
- Use `feat` commits → minor bump, `fix` commits → patch bump
|
|
184
|
+
|
|
145
185
|
## Development Preferences
|
|
146
186
|
|
|
147
187
|
- When executing test scripts with long output, redirect to file for parsing
|
|
@@ -204,6 +244,7 @@ When testing against the real API:
|
|
|
204
244
|
- `docs/assets-and-images.md` — Assets, images, automatic image management
|
|
205
245
|
- `docs/gallery.md` — Campaign gallery
|
|
206
246
|
- `docs/search-and-filtering.md` — Search and filtering
|
|
247
|
+
- `docs/last-sync.md` — Incremental sync using lastSync/sync
|
|
207
248
|
- `docs/pagination.md` — Pagination
|
|
208
249
|
- `docs/error-handling.md` — Exception types and patterns
|
|
209
250
|
- `docs/rate-limiting.md` — Rate limit configuration
|
|
@@ -12,6 +12,7 @@ A modern Python client for the [Kanka API](https://app.kanka.io/api-docs/1.0), t
|
|
|
12
12
|
| [Entity Types Reference](entity-types-reference.md) | All 12 entity types with their specific fields |
|
|
13
13
|
| [Posts](posts.md) | Attach notes and comments to any entity |
|
|
14
14
|
| [Search and Filtering](search-and-filtering.md) | Global search, entities endpoint, list filters |
|
|
15
|
+
| [Last Sync](last-sync.md) | Incremental sync using the lastSync/sync mechanism |
|
|
15
16
|
| [Pagination](pagination.md) | Navigate large result sets |
|
|
16
17
|
| [Assets and Images](assets-and-images.md) | File/link/alias assets, entity images, automatic image management |
|
|
17
18
|
| [Campaign Gallery](gallery.md) | Campaign-level image storage |
|
|
@@ -90,12 +90,27 @@ Get a single entity by its universal `entity_id`. Returns a raw dictionary.
|
|
|
90
90
|
#### entities
|
|
91
91
|
|
|
92
92
|
```python
|
|
93
|
-
entities(
|
|
93
|
+
entities(
|
|
94
|
+
page: int = 1,
|
|
95
|
+
limit: int = 15,
|
|
96
|
+
last_sync: str | None = None,
|
|
97
|
+
**filters,
|
|
98
|
+
) → list[dict[str, Any]]
|
|
94
99
|
```
|
|
95
100
|
|
|
96
101
|
Query the generic `/entities` endpoint. Returns raw dictionaries.
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
| Parameter | Type | Default | Description |
|
|
104
|
+
|-----------|------|---------|-------------|
|
|
105
|
+
| `page` | `int` | `1` | Page number |
|
|
106
|
+
| `limit` | `int` | `15` | Results per page |
|
|
107
|
+
| `last_sync` | `str \| None` | `None` | Only return entities modified after this ISO 8601 timestamp. See [Last Sync](last-sync.md). |
|
|
108
|
+
| `types` | `list[str]` | — | Filter by entity types |
|
|
109
|
+
| `name` | `str` | — | Filter by name |
|
|
110
|
+
| `tags` | `list[int]` | — | Filter by tag IDs |
|
|
111
|
+
| `is_private` | `bool` | — | Filter by privacy |
|
|
112
|
+
| `created_by` | `int` | — | Filter by creator ID |
|
|
113
|
+
| `updated_by` | `int` | — | Filter by updater ID |
|
|
99
114
|
|
|
100
115
|
#### gallery
|
|
101
116
|
|
|
@@ -137,8 +152,13 @@ Delete a gallery image by UUID.
|
|
|
137
152
|
|
|
138
153
|
| Property | Type | Description |
|
|
139
154
|
|----------|------|-------------|
|
|
155
|
+
| `last_entities_meta` | `dict` | Pagination metadata from last `entities()` |
|
|
156
|
+
| `last_entities_links` | `dict` | Pagination links from last `entities()` |
|
|
157
|
+
| `entities_has_next_page` | `bool` | Whether next page exists for `entities()` |
|
|
158
|
+
| `last_entities_sync` | `str \| None` | Sync timestamp from last `entities()` call. See [Last Sync](last-sync.md). |
|
|
140
159
|
| `last_search_meta` | `dict` | Pagination metadata from last `search()` |
|
|
141
160
|
| `last_search_links` | `dict` | Pagination links from last `search()` |
|
|
161
|
+
| `last_search_sync` | `str \| None` | Sync timestamp from last `search()` call |
|
|
142
162
|
| `last_gallery_meta` | `dict` | Pagination metadata from last `gallery()` |
|
|
143
163
|
| `last_gallery_links` | `dict` | Pagination links from last `gallery()` |
|
|
144
164
|
|
|
@@ -191,6 +211,7 @@ list(
|
|
|
191
211
|
page: int = 1,
|
|
192
212
|
limit: int = 30,
|
|
193
213
|
related: bool = False,
|
|
214
|
+
last_sync: str | None = None,
|
|
194
215
|
**filters,
|
|
195
216
|
) → list[T]
|
|
196
217
|
```
|
|
@@ -202,6 +223,7 @@ List entities with optional filtering.
|
|
|
202
223
|
| `page` | `int` | `1` | Page number |
|
|
203
224
|
| `limit` | `int` | `30` | Results per page |
|
|
204
225
|
| `related` | `bool` | `False` | Include posts and attributes |
|
|
226
|
+
| `last_sync` | `str \| None` | `None` | Only return entities modified after this ISO 8601 timestamp. See [Last Sync](last-sync.md). |
|
|
205
227
|
| `name` | `str` | — | Filter by name (partial match) |
|
|
206
228
|
| `type` | `str` | — | Filter by entity type |
|
|
207
229
|
| `types` | `list[str]` | — | Filter by multiple types |
|
|
@@ -211,7 +233,6 @@ List entities with optional filtering.
|
|
|
211
233
|
| `updated_at` | `str` | — | Filter by update date |
|
|
212
234
|
| `created_by` | `int` | — | Filter by creator ID |
|
|
213
235
|
| `updated_by` | `int` | — | Filter by updater ID |
|
|
214
|
-
| `lastSync` | `str` | — | Only entities modified after this ISO 8601 timestamp |
|
|
215
236
|
|
|
216
237
|
**Returns:** List of entity model instances
|
|
217
238
|
|
|
@@ -426,6 +447,7 @@ delete_image(entity_or_id: T | int, is_header: bool = False) → bool
|
|
|
426
447
|
| `pagination_meta` | `dict` | Metadata from last `list()` |
|
|
427
448
|
| `pagination_links` | `dict` | Links from last `list()` |
|
|
428
449
|
| `has_next_page` | `bool` | Whether next page exists |
|
|
450
|
+
| `last_sync` | `str \| None` | Sync timestamp from last `list()` call. See [Last Sync](last-sync.md). |
|
|
429
451
|
| `last_page_meta` | `dict` | Alias for `pagination_meta` |
|
|
430
452
|
| `last_page_links` | `dict` | Alias for `pagination_links` |
|
|
431
453
|
| `last_posts_meta` | `dict` | Metadata from last `list_posts()` |
|
|
@@ -72,15 +72,17 @@ results = client.characters.list(
|
|
|
72
72
|
)
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
### The `
|
|
75
|
+
### The `last_sync` Parameter
|
|
76
76
|
|
|
77
|
-
Use the `
|
|
77
|
+
Use the `last_sync` parameter with an ISO 8601 timestamp to retrieve only entities modified after a specific time. This is useful for keeping a local cache in sync with Kanka without re-fetching everything. See [Last Sync](last-sync.md) for a complete guide including pagination, persistence, and important caveats.
|
|
78
78
|
|
|
79
79
|
```python
|
|
80
|
-
|
|
80
|
+
# Fetch all characters, capturing the sync timestamp
|
|
81
|
+
characters = client.characters.list(page=1, limit=100)
|
|
82
|
+
sync_timestamp = client.characters.last_sync
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
updated = client.characters.list(
|
|
84
|
+
# Later, fetch only characters that changed since then
|
|
85
|
+
updated = client.characters.list(last_sync=sync_timestamp)
|
|
84
86
|
```
|
|
85
87
|
|
|
86
88
|
### Iterating All Pages
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Last Sync
|
|
2
|
+
|
|
3
|
+
The Kanka API provides a synchronization mechanism that lets you efficiently fetch only entities that have changed since your last request. This avoids repeatedly downloading your entire campaign and is essential for applications that need to keep a local cache or database in sync with Kanka.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
Every list response from the Kanka API includes a top-level `sync` timestamp. This timestamp represents the exact moment the server processed your request. To get only entities that changed since then, pass that timestamp back as the `last_sync` parameter on your next request. The API returns only entities created or modified after that timestamp.
|
|
8
|
+
|
|
9
|
+
The workflow is:
|
|
10
|
+
|
|
11
|
+
1. **Initial fetch** — Call the API without `last_sync`. Capture the sync timestamp from the response.
|
|
12
|
+
2. **Subsequent fetches** — Call the API with `last_sync` set to the previously captured value. Only modified entities are returned. Capture the new sync timestamp for next time.
|
|
13
|
+
3. **Repeat** — Each call gives you a new sync timestamp to use on the next call.
|
|
14
|
+
|
|
15
|
+
## Using last_sync with Entity Managers
|
|
16
|
+
|
|
17
|
+
The `last_sync` parameter is a named parameter on all entity manager `list()` calls. After each call, the sync timestamp is available via the `last_sync` property.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from kanka import KankaClient
|
|
21
|
+
|
|
22
|
+
client = KankaClient(token="your-token", campaign_id=12345)
|
|
23
|
+
|
|
24
|
+
# Initial fetch — get all characters
|
|
25
|
+
characters = client.characters.list(page=1, limit=100)
|
|
26
|
+
sync_timestamp = client.characters.last_sync
|
|
27
|
+
print(f"Fetched {len(characters)} characters")
|
|
28
|
+
print(f"Sync timestamp: {sync_timestamp}")
|
|
29
|
+
|
|
30
|
+
# ... later, fetch only characters that changed ...
|
|
31
|
+
updated = client.characters.list(last_sync=sync_timestamp)
|
|
32
|
+
new_sync = client.characters.last_sync
|
|
33
|
+
print(f"{len(updated)} characters changed since last sync")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Using last_sync with the Generic Entities Endpoint
|
|
37
|
+
|
|
38
|
+
The generic `client.entities()` endpoint also supports `last_sync` as a named parameter. This is useful when you want to sync all entity types at once rather than syncing each type separately. The sync timestamp is available via the `last_entities_sync` property.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Initial fetch — get all entities
|
|
42
|
+
entities = client.entities(page=1, limit=100)
|
|
43
|
+
sync_timestamp = client.last_entities_sync
|
|
44
|
+
|
|
45
|
+
# ... later, fetch only entities that changed ...
|
|
46
|
+
updated = client.entities(last_sync=sync_timestamp)
|
|
47
|
+
new_sync = client.last_entities_sync
|
|
48
|
+
print(f"{len(updated)} entities changed since last sync")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Paginating with last_sync
|
|
52
|
+
|
|
53
|
+
When your campaign has more entities than fit in a single page, you need to paginate through all pages on both the initial fetch and on subsequent syncs. Use the same `last_sync` value across all pages of a single sync operation, and capture the sync timestamp from the **first** page to use for the next sync.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
def sync_all_characters(client, last_sync=None):
|
|
57
|
+
"""Fetch all characters, optionally only those changed since last_sync."""
|
|
58
|
+
all_characters = []
|
|
59
|
+
page = 1
|
|
60
|
+
next_sync = None
|
|
61
|
+
|
|
62
|
+
while True:
|
|
63
|
+
batch = client.characters.list(
|
|
64
|
+
page=page, limit=100, last_sync=last_sync
|
|
65
|
+
)
|
|
66
|
+
all_characters.extend(batch)
|
|
67
|
+
|
|
68
|
+
# Capture sync timestamp from the first page
|
|
69
|
+
if page == 1:
|
|
70
|
+
next_sync = client.characters.last_sync
|
|
71
|
+
|
|
72
|
+
if not client.characters.has_next_page:
|
|
73
|
+
break
|
|
74
|
+
page += 1
|
|
75
|
+
|
|
76
|
+
return all_characters, next_sync
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Initial full sync
|
|
80
|
+
characters, sync_timestamp = sync_all_characters(client)
|
|
81
|
+
print(f"Full sync: {len(characters)} characters")
|
|
82
|
+
|
|
83
|
+
# ... time passes, entities are modified in Kanka ...
|
|
84
|
+
|
|
85
|
+
# Incremental sync — only fetches what changed
|
|
86
|
+
changed, sync_timestamp = sync_all_characters(client, last_sync=sync_timestamp)
|
|
87
|
+
print(f"Incremental sync: {len(changed)} characters changed")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Persisting the Sync Timestamp
|
|
91
|
+
|
|
92
|
+
For the sync pattern to work across application restarts, you need to persist the sync timestamp. How you store it depends on your application — a file, database, or configuration store all work:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import json
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
|
|
98
|
+
SYNC_FILE = Path("kanka_sync_state.json")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_sync_state():
|
|
102
|
+
"""Load the last sync timestamp from disk."""
|
|
103
|
+
if SYNC_FILE.exists():
|
|
104
|
+
return json.loads(SYNC_FILE.read_text())
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def save_sync_state(state):
|
|
109
|
+
"""Save the sync timestamp to disk."""
|
|
110
|
+
SYNC_FILE.write_text(json.dumps(state, indent=2))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Load previous state
|
|
114
|
+
state = load_sync_state()
|
|
115
|
+
|
|
116
|
+
# Sync characters
|
|
117
|
+
characters, new_sync = sync_all_characters(
|
|
118
|
+
client, last_sync=state.get("characters_sync")
|
|
119
|
+
)
|
|
120
|
+
state["characters_sync"] = new_sync
|
|
121
|
+
|
|
122
|
+
# Save state for next run
|
|
123
|
+
save_sync_state(state)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Important Notes
|
|
127
|
+
|
|
128
|
+
- **The sync timestamp is server-generated.** Always use the value from the API response rather than generating your own timestamp. This avoids clock skew issues between your machine and the Kanka server.
|
|
129
|
+
- **`last_sync` filters by modification time.** It returns entities that were created or updated after the given timestamp. It does **not** tell you about deleted entities.
|
|
130
|
+
- **Deleted entities are not included.** The `last_sync` response only contains entities that still exist. If you maintain a local cache, you may need a separate mechanism to detect deletions (e.g., periodically doing a full sync and comparing IDs).
|
|
131
|
+
- **Use ISO 8601 format.** The sync value from the API is already in the correct format. If you construct a timestamp manually, use full ISO 8601 with timezone (e.g., `2026-02-25T01:35:39.768695Z`).
|
|
132
|
+
- **Works with all filters.** You can combine `last_sync` with other filters like `type`, `name`, or `tags`. The API returns entities matching all filters that were also modified after the `last_sync` timestamp.
|
|
@@ -83,6 +83,14 @@ Each list endpoint stores its own pagination state:
|
|
|
83
83
|
| `manager.last_assets_meta` | Metadata from last `list_assets()` |
|
|
84
84
|
| `manager.last_assets_links` | Links from last `list_assets()` |
|
|
85
85
|
|
|
86
|
+
### Entities
|
|
87
|
+
|
|
88
|
+
| Property | Description |
|
|
89
|
+
|----------|-------------|
|
|
90
|
+
| `client.last_entities_meta` | Metadata from last `entities()` |
|
|
91
|
+
| `client.last_entities_links` | Links from last `entities()` |
|
|
92
|
+
| `client.entities_has_next_page` | `True` if next page exists |
|
|
93
|
+
|
|
86
94
|
### Gallery
|
|
87
95
|
|
|
88
96
|
| Property | Description |
|
|
@@ -137,7 +137,7 @@ results = client.characters.list(
|
|
|
137
137
|
| `updated_at` | `str` | Filter by update date (ISO format, supports `>=` prefix) |
|
|
138
138
|
| `created_by` | `int` | Filter by creator user ID |
|
|
139
139
|
| `updated_by` | `int` | Filter by last updater user ID |
|
|
140
|
-
| `
|
|
140
|
+
| `last_sync` | `str` | Only return entities modified after this ISO 8601 timestamp |
|
|
141
141
|
|
|
142
142
|
### Filter Value Handling
|
|
143
143
|
|
|
@@ -147,14 +147,7 @@ results = client.characters.list(
|
|
|
147
147
|
|
|
148
148
|
### Sync Filter
|
|
149
149
|
|
|
150
|
-
The `
|
|
151
|
-
|
|
152
|
-
```python
|
|
153
|
-
from datetime import datetime, timedelta, timezone
|
|
154
|
-
|
|
155
|
-
last_sync = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
|
|
156
|
-
updated_chars = client.characters.list(lastSync=last_sync, related=True)
|
|
157
|
-
```
|
|
150
|
+
The `last_sync` parameter returns only entities modified since a given timestamp. Each API response includes a sync timestamp (available via the `last_sync` property) that you should capture and reuse. See [Last Sync](last-sync.md) for a complete guide including the full sync workflow, pagination, persistence, and important caveats.
|
|
158
151
|
|
|
159
152
|
## Choosing the Right Approach
|
|
160
153
|
|
|
@@ -298,9 +298,10 @@ class KankaClient:
|
|
|
298
298
|
params: dict[str, int | str] = {"page": page}
|
|
299
299
|
response = self._request("GET", f"search/{term}", params=params)
|
|
300
300
|
|
|
301
|
-
# Store pagination metadata for access if needed
|
|
301
|
+
# Store pagination metadata and sync timestamp for access if needed
|
|
302
302
|
self._last_search_meta = response.get("meta", {})
|
|
303
303
|
self._last_search_links = response.get("links", {})
|
|
304
|
+
self._last_search_sync: str | None = response.get("sync")
|
|
304
305
|
|
|
305
306
|
return [SearchResult(**item) for item in response["data"]]
|
|
306
307
|
|
|
@@ -321,19 +322,42 @@ class KankaClient:
|
|
|
321
322
|
response = self._request("GET", f"entities/{entity_id}")
|
|
322
323
|
return response["data"] # type: ignore[no-any-return]
|
|
323
324
|
|
|
324
|
-
def entities(
|
|
325
|
+
def entities(
|
|
326
|
+
self,
|
|
327
|
+
page: int = 1,
|
|
328
|
+
limit: int = 15,
|
|
329
|
+
last_sync: str | None = None,
|
|
330
|
+
**filters,
|
|
331
|
+
) -> list[dict[str, Any]]:
|
|
325
332
|
"""Access the /entities endpoint with filters.
|
|
326
333
|
|
|
327
334
|
This endpoint provides a unified way to query entities across all types
|
|
328
335
|
with various filtering options.
|
|
329
336
|
|
|
330
337
|
Args:
|
|
338
|
+
page: Page number (default: 1)
|
|
339
|
+
limit: Number of results per page (default: 15)
|
|
340
|
+
last_sync: ISO 8601 timestamp; only return entities modified after
|
|
341
|
+
this time. Use the ``last_entities_sync`` property from a
|
|
342
|
+
previous call to implement incremental sync.
|
|
331
343
|
**filters: Filter parameters like types, name, is_private, tags
|
|
332
344
|
|
|
333
345
|
Returns:
|
|
334
346
|
List of entity data
|
|
347
|
+
|
|
348
|
+
Note:
|
|
349
|
+
After calling entities(), the following properties are available:
|
|
350
|
+
- client.last_entities_meta: Contains total count, current page, etc.
|
|
351
|
+
- client.last_entities_links: Contains URLs for first, last, prev, next
|
|
352
|
+
- client.entities_has_next_page: True if more pages are available
|
|
353
|
+
- client.last_entities_sync: Server-generated sync timestamp for use
|
|
354
|
+
with the ``last_sync`` parameter on subsequent calls
|
|
335
355
|
"""
|
|
336
|
-
params: dict[str, int | str] = {}
|
|
356
|
+
params: dict[str, int | str] = {"page": page, "limit": limit}
|
|
357
|
+
|
|
358
|
+
# Add lastSync parameter for incremental sync
|
|
359
|
+
if last_sync is not None:
|
|
360
|
+
params["lastSync"] = last_sync
|
|
337
361
|
|
|
338
362
|
# Handle special filters
|
|
339
363
|
if "types" in filters and isinstance(filters["types"], list):
|
|
@@ -355,6 +379,9 @@ class KankaClient:
|
|
|
355
379
|
params[key] = filters[key]
|
|
356
380
|
|
|
357
381
|
response = self._request("GET", "entities", params=params)
|
|
382
|
+
self._last_entities_meta = response.get("meta", {})
|
|
383
|
+
self._last_entities_links = response.get("links", {})
|
|
384
|
+
self._last_entities_sync: str | None = response.get("sync")
|
|
358
385
|
return response["data"] # type: ignore[no-any-return]
|
|
359
386
|
|
|
360
387
|
def _parse_rate_limit_headers(self, response) -> float | None:
|
|
@@ -692,3 +719,49 @@ class KankaClient:
|
|
|
692
719
|
Dict[str, Any]: Links for pagination including first, last, prev, next URLs
|
|
693
720
|
"""
|
|
694
721
|
return getattr(self, "_last_search_links", {})
|
|
722
|
+
|
|
723
|
+
@property
|
|
724
|
+
def last_search_sync(self) -> str | None:
|
|
725
|
+
"""Get the sync timestamp from the last search() call.
|
|
726
|
+
|
|
727
|
+
Returns:
|
|
728
|
+
The sync timestamp string, or None if search() has not been called.
|
|
729
|
+
"""
|
|
730
|
+
return getattr(self, "_last_search_sync", None)
|
|
731
|
+
|
|
732
|
+
@property
|
|
733
|
+
def last_entities_meta(self) -> dict[str, Any]:
|
|
734
|
+
"""Get metadata from the last entities() call.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
Dict[str, Any]: Pagination metadata including current_page, from, to,
|
|
738
|
+
last_page, per_page, total
|
|
739
|
+
"""
|
|
740
|
+
return getattr(self, "_last_entities_meta", {})
|
|
741
|
+
|
|
742
|
+
@property
|
|
743
|
+
def last_entities_links(self) -> dict[str, Any]:
|
|
744
|
+
"""Get pagination links from the last entities() call.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
Dict[str, Any]: Links for pagination including first, last, prev, next URLs
|
|
748
|
+
"""
|
|
749
|
+
return getattr(self, "_last_entities_links", {})
|
|
750
|
+
|
|
751
|
+
@property
|
|
752
|
+
def last_entities_sync(self) -> str | None:
|
|
753
|
+
"""Get the sync timestamp from the last entities() call.
|
|
754
|
+
|
|
755
|
+
This is a server-generated ISO 8601 timestamp representing when the
|
|
756
|
+
response was produced. Pass it as the ``last_sync`` parameter on a
|
|
757
|
+
subsequent ``entities()`` call to fetch only entities modified since then.
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
The sync timestamp string, or None if entities() has not been called.
|
|
761
|
+
"""
|
|
762
|
+
return getattr(self, "_last_entities_sync", None)
|
|
763
|
+
|
|
764
|
+
@property
|
|
765
|
+
def entities_has_next_page(self) -> bool:
|
|
766
|
+
"""Check if more pages are available from the last entities() call."""
|
|
767
|
+
return bool(getattr(self, "_last_entities_links", {}).get("next"))
|
|
@@ -80,7 +80,12 @@ class EntityManager[T: Entity]:
|
|
|
80
80
|
return bool(self.pagination_links.get("next"))
|
|
81
81
|
|
|
82
82
|
def list(
|
|
83
|
-
self,
|
|
83
|
+
self,
|
|
84
|
+
page: int = 1,
|
|
85
|
+
limit: int = 30,
|
|
86
|
+
related: bool = False,
|
|
87
|
+
last_sync: str | None = None,
|
|
88
|
+
**filters,
|
|
84
89
|
) -> list[T]:
|
|
85
90
|
"""List entities with optional filters.
|
|
86
91
|
|
|
@@ -88,6 +93,9 @@ class EntityManager[T: Entity]:
|
|
|
88
93
|
page: Page number (default: 1)
|
|
89
94
|
limit: Number of results per page (default: 30)
|
|
90
95
|
related: Include related data (posts, attributes, etc.)
|
|
96
|
+
last_sync: ISO 8601 timestamp; only return entities modified after
|
|
97
|
+
this time. Use the ``last_sync`` property from a previous call
|
|
98
|
+
to implement incremental sync.
|
|
91
99
|
**filters: Additional filters supported by the API
|
|
92
100
|
|
|
93
101
|
Supported filters:
|
|
@@ -104,10 +112,12 @@ class EntityManager[T: Entity]:
|
|
|
104
112
|
List of entity instances
|
|
105
113
|
|
|
106
114
|
Note:
|
|
107
|
-
|
|
115
|
+
After calling list(), the following properties are available:
|
|
108
116
|
- manager.pagination_meta: Contains total count, current page, etc.
|
|
109
117
|
- manager.pagination_links: Contains URLs for first, last, prev, next
|
|
110
118
|
- manager.has_next_page: True if more pages are available
|
|
119
|
+
- manager.last_sync: Server-generated sync timestamp for use with
|
|
120
|
+
the ``last_sync`` parameter on subsequent calls
|
|
111
121
|
|
|
112
122
|
Examples:
|
|
113
123
|
# Get all public characters
|
|
@@ -154,6 +164,10 @@ class EntityManager[T: Entity]:
|
|
|
154
164
|
if related:
|
|
155
165
|
params["related"] = 1
|
|
156
166
|
|
|
167
|
+
# Add lastSync parameter for incremental sync
|
|
168
|
+
if last_sync is not None:
|
|
169
|
+
params["lastSync"] = last_sync
|
|
170
|
+
|
|
157
171
|
# Add filters
|
|
158
172
|
for key, value in filters.items():
|
|
159
173
|
if value is not None:
|
|
@@ -175,9 +189,10 @@ class EntityManager[T: Entity]:
|
|
|
175
189
|
|
|
176
190
|
response = self.client._request("GET", self.endpoint, params=params)
|
|
177
191
|
|
|
178
|
-
# Store pagination metadata
|
|
192
|
+
# Store pagination metadata and sync timestamp for access if needed
|
|
179
193
|
self._last_meta = response.get("meta", {})
|
|
180
194
|
self._last_links = response.get("links", {})
|
|
195
|
+
self._last_sync: str | None = response.get("sync")
|
|
181
196
|
|
|
182
197
|
return [self.model(**item) for item in response["data"]]
|
|
183
198
|
|
|
@@ -379,6 +394,19 @@ class EntityManager[T: Entity]:
|
|
|
379
394
|
"""
|
|
380
395
|
return getattr(self, "_last_links", {})
|
|
381
396
|
|
|
397
|
+
@property
|
|
398
|
+
def last_sync(self) -> str | None:
|
|
399
|
+
"""Get the sync timestamp from the last list() call.
|
|
400
|
+
|
|
401
|
+
This is a server-generated ISO 8601 timestamp representing when the
|
|
402
|
+
response was produced. Pass it as the ``last_sync`` parameter on a
|
|
403
|
+
subsequent ``list()`` call to fetch only entities modified since then.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
The sync timestamp string, or None if list() has not been called.
|
|
407
|
+
"""
|
|
408
|
+
return getattr(self, "_last_sync", None)
|
|
409
|
+
|
|
382
410
|
def _extract_entity_id(self, entity_or_id: T | int) -> int:
|
|
383
411
|
"""Extract entity_id from an entity object or integer.
|
|
384
412
|
|