python-kanka 2.5.0__tar.gz → 2.6.1__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.5.0 → python_kanka-2.6.1}/.bumpversion.cfg +1 -1
- python_kanka-2.6.1/.github/workflows/docs.yml +63 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/.github/workflows/publish.yml +2 -2
- {python_kanka-2.5.0 → python_kanka-2.6.1}/CLAUDE.md +41 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/Makefile +22 -1
- {python_kanka-2.5.0 → python_kanka-2.6.1}/PKG-INFO +2 -2
- python_kanka-2.6.1/docs/CNAME +1 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/README.md +1 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/api-reference.md +12 -2
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/entities.md +7 -5
- python_kanka-2.6.1/docs/last-sync.md +132 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/search-and-filtering.md +2 -9
- python_kanka-2.6.1/mkdocs.yml +55 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/pyproject.toml +6 -2
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/_version.py +1 -1
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/client.py +40 -3
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/managers.py +31 -3
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entities_api_integration.py +136 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_client.py +90 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_managers.py +54 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/utils.py +10 -1
- {python_kanka-2.5.0 → python_kanka-2.6.1}/uv.lock +285 -1
- {python_kanka-2.5.0 → python_kanka-2.6.1}/.github/renovate.json +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/.github/workflows/ci.yml +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/.gitignore +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/.pre-commit-config.yaml +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/CHANGELOG.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/LICENSE +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/MANIFEST.in +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/PUBLISHING.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/README.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/assets-and-images.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/core-concepts.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/debug-mode.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/entity-types-reference.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/error-handling.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/gallery.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/getting-started.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/known-limitations.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/pagination.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/posts.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/rate-limiting.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/pytest.ini +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/__init__.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/exceptions.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/__init__.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/base.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/common.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/entities.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/py.typed +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/types.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/__init__.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/README.md +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/__init__.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/base.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/run_integration_tests.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/setup_test_env.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_calendars_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_calendars_simple.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_characters_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_creatures_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entity_assets_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entity_images_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entity_tags_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_events_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_families_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_gallery_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_journals_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_locations_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_mentions_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_notes_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_organisations_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_posts_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_quests_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_races_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_rate_limiting.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_search_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_tag_colour.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_tags_integration.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/mypy.ini +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_client_advanced.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_entity_models.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_exceptions.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_models.py +0 -0
- {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_models_advanced.py +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Deploy Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
pages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: pages
|
|
15
|
+
cancel-in-progress: false
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v7
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.14
|
|
28
|
+
|
|
29
|
+
- name: Install docs dependencies
|
|
30
|
+
run: uv sync --group docs
|
|
31
|
+
|
|
32
|
+
- name: Prepare index page
|
|
33
|
+
run: mv docs/README.md docs/index.md
|
|
34
|
+
|
|
35
|
+
- name: Build MkDocs site
|
|
36
|
+
run: uv run mkdocs build --strict
|
|
37
|
+
|
|
38
|
+
- name: Restore README and copy raw markdown to site
|
|
39
|
+
run: |
|
|
40
|
+
mv docs/index.md docs/README.md
|
|
41
|
+
for f in docs/*.md; do
|
|
42
|
+
name="$(basename "$f")"
|
|
43
|
+
if [ "$name" != "README.md" ]; then
|
|
44
|
+
cp "$f" site/
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
cp docs/README.md site/llms.txt
|
|
48
|
+
|
|
49
|
+
- name: Upload Pages artifact
|
|
50
|
+
uses: actions/upload-pages-artifact@v3
|
|
51
|
+
with:
|
|
52
|
+
path: site/
|
|
53
|
+
|
|
54
|
+
deploy:
|
|
55
|
+
needs: build
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
environment:
|
|
58
|
+
name: github-pages
|
|
59
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
60
|
+
steps:
|
|
61
|
+
- name: Deploy to GitHub Pages
|
|
62
|
+
id: deployment
|
|
63
|
+
uses: actions/deploy-pages@v4
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: install test lint format typecheck check clean coverage help sync build
|
|
1
|
+
.PHONY: install test lint format typecheck check clean coverage help sync build docs-serve docs-build
|
|
2
2
|
|
|
3
3
|
# Default target
|
|
4
4
|
help:
|
|
@@ -13,6 +13,8 @@ help:
|
|
|
13
13
|
@echo " make clean - Clean up temporary files"
|
|
14
14
|
@echo " make coverage - Run tests with coverage report"
|
|
15
15
|
@echo " make build - Build the package"
|
|
16
|
+
@echo " make docs-serve - Serve docs locally for preview"
|
|
17
|
+
@echo " make docs-build - Build docs locally"
|
|
16
18
|
|
|
17
19
|
# Install development dependencies and sync with lock file
|
|
18
20
|
install:
|
|
@@ -71,3 +73,22 @@ coverage:
|
|
|
71
73
|
# Build the package
|
|
72
74
|
build:
|
|
73
75
|
uv build
|
|
76
|
+
|
|
77
|
+
# Serve docs locally for preview
|
|
78
|
+
docs-serve:
|
|
79
|
+
mv docs/README.md docs/index.md
|
|
80
|
+
uv run mkdocs serve || true
|
|
81
|
+
mv docs/index.md docs/README.md
|
|
82
|
+
|
|
83
|
+
# Build docs locally
|
|
84
|
+
docs-build:
|
|
85
|
+
mv docs/README.md docs/index.md
|
|
86
|
+
uv run mkdocs build --strict
|
|
87
|
+
mv docs/index.md docs/README.md
|
|
88
|
+
@for f in docs/*.md; do \
|
|
89
|
+
name=$$(basename "$$f"); \
|
|
90
|
+
if [ "$$name" != "README.md" ]; then \
|
|
91
|
+
cp "$$f" site/; \
|
|
92
|
+
fi; \
|
|
93
|
+
done
|
|
94
|
+
cp docs/README.md site/llms.txt
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-kanka
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
4
4
|
Summary: Python client for the Kanka API
|
|
5
5
|
Project-URL: Homepage, https://github.com/ervwalter/python-kanka
|
|
6
|
-
Project-URL: Documentation, https://
|
|
6
|
+
Project-URL: Documentation, https://python-kanka.ewal.dev/
|
|
7
7
|
Project-URL: Repository, https://github.com/ervwalter/python-kanka
|
|
8
8
|
Project-URL: Issues, https://github.com/ervwalter/python-kanka/issues
|
|
9
9
|
Author-email: Erv Walter <erv@ewal.net>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python-kanka.ewal.dev
|
|
@@ -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,7 +90,12 @@ 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.
|
|
@@ -99,6 +104,7 @@ Query the generic `/entities` endpoint. Returns raw dictionaries.
|
|
|
99
104
|
|-----------|------|---------|-------------|
|
|
100
105
|
| `page` | `int` | `1` | Page number |
|
|
101
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). |
|
|
102
108
|
| `types` | `list[str]` | — | Filter by entity types |
|
|
103
109
|
| `name` | `str` | — | Filter by name |
|
|
104
110
|
| `tags` | `list[int]` | — | Filter by tag IDs |
|
|
@@ -149,8 +155,10 @@ Delete a gallery image by UUID.
|
|
|
149
155
|
| `last_entities_meta` | `dict` | Pagination metadata from last `entities()` |
|
|
150
156
|
| `last_entities_links` | `dict` | Pagination links from last `entities()` |
|
|
151
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). |
|
|
152
159
|
| `last_search_meta` | `dict` | Pagination metadata from last `search()` |
|
|
153
160
|
| `last_search_links` | `dict` | Pagination links from last `search()` |
|
|
161
|
+
| `last_search_sync` | `str \| None` | Sync timestamp from last `search()` call |
|
|
154
162
|
| `last_gallery_meta` | `dict` | Pagination metadata from last `gallery()` |
|
|
155
163
|
| `last_gallery_links` | `dict` | Pagination links from last `gallery()` |
|
|
156
164
|
|
|
@@ -203,6 +211,7 @@ list(
|
|
|
203
211
|
page: int = 1,
|
|
204
212
|
limit: int = 30,
|
|
205
213
|
related: bool = False,
|
|
214
|
+
last_sync: str | None = None,
|
|
206
215
|
**filters,
|
|
207
216
|
) → list[T]
|
|
208
217
|
```
|
|
@@ -214,6 +223,7 @@ List entities with optional filtering.
|
|
|
214
223
|
| `page` | `int` | `1` | Page number |
|
|
215
224
|
| `limit` | `int` | `30` | Results per page |
|
|
216
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). |
|
|
217
227
|
| `name` | `str` | — | Filter by name (partial match) |
|
|
218
228
|
| `type` | `str` | — | Filter by entity type |
|
|
219
229
|
| `types` | `list[str]` | — | Filter by multiple types |
|
|
@@ -223,7 +233,6 @@ List entities with optional filtering.
|
|
|
223
233
|
| `updated_at` | `str` | — | Filter by update date |
|
|
224
234
|
| `created_by` | `int` | — | Filter by creator ID |
|
|
225
235
|
| `updated_by` | `int` | — | Filter by updater ID |
|
|
226
|
-
| `lastSync` | `str` | — | Only entities modified after this ISO 8601 timestamp |
|
|
227
236
|
|
|
228
237
|
**Returns:** List of entity model instances
|
|
229
238
|
|
|
@@ -438,6 +447,7 @@ delete_image(entity_or_id: T | int, is_header: bool = False) → bool
|
|
|
438
447
|
| `pagination_meta` | `dict` | Metadata from last `list()` |
|
|
439
448
|
| `pagination_links` | `dict` | Links from last `list()` |
|
|
440
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). |
|
|
441
451
|
| `last_page_meta` | `dict` | Alias for `pagination_meta` |
|
|
442
452
|
| `last_page_links` | `dict` | Alias for `pagination_links` |
|
|
443
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.
|
|
@@ -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
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
site_name: python-kanka
|
|
2
|
+
site_url: https://python-kanka.ewal.dev/
|
|
3
|
+
site_description: A modern Python client for the Kanka API
|
|
4
|
+
repo_url: https://github.com/ervwalter/python-kanka
|
|
5
|
+
repo_name: ervwalter/python-kanka
|
|
6
|
+
|
|
7
|
+
theme:
|
|
8
|
+
name: material
|
|
9
|
+
features:
|
|
10
|
+
- navigation.sections
|
|
11
|
+
- navigation.expand
|
|
12
|
+
- content.code.copy
|
|
13
|
+
- search.suggest
|
|
14
|
+
palette:
|
|
15
|
+
- scheme: default
|
|
16
|
+
primary: indigo
|
|
17
|
+
accent: indigo
|
|
18
|
+
toggle:
|
|
19
|
+
icon: material/brightness-7
|
|
20
|
+
name: Switch to dark mode
|
|
21
|
+
- scheme: slate
|
|
22
|
+
primary: indigo
|
|
23
|
+
accent: indigo
|
|
24
|
+
toggle:
|
|
25
|
+
icon: material/brightness-4
|
|
26
|
+
name: Switch to light mode
|
|
27
|
+
|
|
28
|
+
nav:
|
|
29
|
+
- Home: index.md
|
|
30
|
+
- Guides:
|
|
31
|
+
- Getting Started: getting-started.md
|
|
32
|
+
- Core Concepts: core-concepts.md
|
|
33
|
+
- Entity CRUD Operations: entities.md
|
|
34
|
+
- Entity Types Reference: entity-types-reference.md
|
|
35
|
+
- Posts: posts.md
|
|
36
|
+
- Search and Filtering: search-and-filtering.md
|
|
37
|
+
- Last Sync: last-sync.md
|
|
38
|
+
- Pagination: pagination.md
|
|
39
|
+
- Assets and Images: assets-and-images.md
|
|
40
|
+
- Campaign Gallery: gallery.md
|
|
41
|
+
- Error Handling: error-handling.md
|
|
42
|
+
- Rate Limiting: rate-limiting.md
|
|
43
|
+
- Debug Mode: debug-mode.md
|
|
44
|
+
- Known Limitations: known-limitations.md
|
|
45
|
+
- Reference:
|
|
46
|
+
- API Reference: api-reference.md
|
|
47
|
+
|
|
48
|
+
markdown_extensions:
|
|
49
|
+
- pymdownx.highlight:
|
|
50
|
+
anchor_linenums: true
|
|
51
|
+
- pymdownx.superfences
|
|
52
|
+
- admonition
|
|
53
|
+
- tables
|
|
54
|
+
- toc:
|
|
55
|
+
permalink: true
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-kanka"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.6.1"
|
|
8
8
|
description = "Python client for the Kanka API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -33,11 +33,15 @@ dependencies = [
|
|
|
33
33
|
|
|
34
34
|
[project.urls]
|
|
35
35
|
Homepage = "https://github.com/ervwalter/python-kanka"
|
|
36
|
-
Documentation = "https://
|
|
36
|
+
Documentation = "https://python-kanka.ewal.dev/"
|
|
37
37
|
Repository = "https://github.com/ervwalter/python-kanka"
|
|
38
38
|
Issues = "https://github.com/ervwalter/python-kanka/issues"
|
|
39
39
|
|
|
40
40
|
[dependency-groups]
|
|
41
|
+
docs = [
|
|
42
|
+
"mkdocs>=1.6",
|
|
43
|
+
"mkdocs-material>=9.5",
|
|
44
|
+
]
|
|
41
45
|
dev = [
|
|
42
46
|
"pytest==9.0.2",
|
|
43
47
|
"pytest-asyncio==1.3.0",
|
|
@@ -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
|
|
|
@@ -322,7 +323,11 @@ class KankaClient:
|
|
|
322
323
|
return response["data"] # type: ignore[no-any-return]
|
|
323
324
|
|
|
324
325
|
def entities(
|
|
325
|
-
self,
|
|
326
|
+
self,
|
|
327
|
+
page: int = 1,
|
|
328
|
+
limit: int = 15,
|
|
329
|
+
last_sync: str | None = None,
|
|
330
|
+
**filters,
|
|
326
331
|
) -> list[dict[str, Any]]:
|
|
327
332
|
"""Access the /entities endpoint with filters.
|
|
328
333
|
|
|
@@ -332,19 +337,28 @@ class KankaClient:
|
|
|
332
337
|
Args:
|
|
333
338
|
page: Page number (default: 1)
|
|
334
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.
|
|
335
343
|
**filters: Filter parameters like types, name, is_private, tags
|
|
336
344
|
|
|
337
345
|
Returns:
|
|
338
346
|
List of entity data
|
|
339
347
|
|
|
340
348
|
Note:
|
|
341
|
-
|
|
349
|
+
After calling entities(), the following properties are available:
|
|
342
350
|
- client.last_entities_meta: Contains total count, current page, etc.
|
|
343
351
|
- client.last_entities_links: Contains URLs for first, last, prev, next
|
|
344
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
|
|
345
355
|
"""
|
|
346
356
|
params: dict[str, int | str] = {"page": page, "limit": limit}
|
|
347
357
|
|
|
358
|
+
# Add lastSync parameter for incremental sync
|
|
359
|
+
if last_sync is not None:
|
|
360
|
+
params["lastSync"] = last_sync
|
|
361
|
+
|
|
348
362
|
# Handle special filters
|
|
349
363
|
if "types" in filters and isinstance(filters["types"], list):
|
|
350
364
|
params["types"] = ",".join(filters["types"])
|
|
@@ -367,6 +381,7 @@ class KankaClient:
|
|
|
367
381
|
response = self._request("GET", "entities", params=params)
|
|
368
382
|
self._last_entities_meta = response.get("meta", {})
|
|
369
383
|
self._last_entities_links = response.get("links", {})
|
|
384
|
+
self._last_entities_sync: str | None = response.get("sync")
|
|
370
385
|
return response["data"] # type: ignore[no-any-return]
|
|
371
386
|
|
|
372
387
|
def _parse_rate_limit_headers(self, response) -> float | None:
|
|
@@ -705,6 +720,15 @@ class KankaClient:
|
|
|
705
720
|
"""
|
|
706
721
|
return getattr(self, "_last_search_links", {})
|
|
707
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
|
+
|
|
708
732
|
@property
|
|
709
733
|
def last_entities_meta(self) -> dict[str, Any]:
|
|
710
734
|
"""Get metadata from the last entities() call.
|
|
@@ -724,6 +748,19 @@ class KankaClient:
|
|
|
724
748
|
"""
|
|
725
749
|
return getattr(self, "_last_entities_links", {})
|
|
726
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
|
+
|
|
727
764
|
@property
|
|
728
765
|
def entities_has_next_page(self) -> bool:
|
|
729
766
|
"""Check if more pages are available from the last entities() call."""
|