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.
Files changed (85) hide show
  1. {python_kanka-2.5.0 → python_kanka-2.6.1}/.bumpversion.cfg +1 -1
  2. python_kanka-2.6.1/.github/workflows/docs.yml +63 -0
  3. {python_kanka-2.5.0 → python_kanka-2.6.1}/.github/workflows/publish.yml +2 -2
  4. {python_kanka-2.5.0 → python_kanka-2.6.1}/CLAUDE.md +41 -0
  5. {python_kanka-2.5.0 → python_kanka-2.6.1}/Makefile +22 -1
  6. {python_kanka-2.5.0 → python_kanka-2.6.1}/PKG-INFO +2 -2
  7. python_kanka-2.6.1/docs/CNAME +1 -0
  8. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/README.md +1 -0
  9. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/api-reference.md +12 -2
  10. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/entities.md +7 -5
  11. python_kanka-2.6.1/docs/last-sync.md +132 -0
  12. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/search-and-filtering.md +2 -9
  13. python_kanka-2.6.1/mkdocs.yml +55 -0
  14. {python_kanka-2.5.0 → python_kanka-2.6.1}/pyproject.toml +6 -2
  15. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/_version.py +1 -1
  16. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/client.py +40 -3
  17. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/managers.py +31 -3
  18. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entities_api_integration.py +136 -0
  19. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_client.py +90 -0
  20. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_managers.py +54 -0
  21. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/utils.py +10 -1
  22. {python_kanka-2.5.0 → python_kanka-2.6.1}/uv.lock +285 -1
  23. {python_kanka-2.5.0 → python_kanka-2.6.1}/.github/renovate.json +0 -0
  24. {python_kanka-2.5.0 → python_kanka-2.6.1}/.github/workflows/ci.yml +0 -0
  25. {python_kanka-2.5.0 → python_kanka-2.6.1}/.gitignore +0 -0
  26. {python_kanka-2.5.0 → python_kanka-2.6.1}/.pre-commit-config.yaml +0 -0
  27. {python_kanka-2.5.0 → python_kanka-2.6.1}/CHANGELOG.md +0 -0
  28. {python_kanka-2.5.0 → python_kanka-2.6.1}/LICENSE +0 -0
  29. {python_kanka-2.5.0 → python_kanka-2.6.1}/MANIFEST.in +0 -0
  30. {python_kanka-2.5.0 → python_kanka-2.6.1}/PUBLISHING.md +0 -0
  31. {python_kanka-2.5.0 → python_kanka-2.6.1}/README.md +0 -0
  32. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/assets-and-images.md +0 -0
  33. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/core-concepts.md +0 -0
  34. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/debug-mode.md +0 -0
  35. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/entity-types-reference.md +0 -0
  36. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/error-handling.md +0 -0
  37. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/gallery.md +0 -0
  38. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/getting-started.md +0 -0
  39. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/known-limitations.md +0 -0
  40. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/pagination.md +0 -0
  41. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/posts.md +0 -0
  42. {python_kanka-2.5.0 → python_kanka-2.6.1}/docs/rate-limiting.md +0 -0
  43. {python_kanka-2.5.0 → python_kanka-2.6.1}/pytest.ini +0 -0
  44. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/__init__.py +0 -0
  45. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/exceptions.py +0 -0
  46. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/__init__.py +0 -0
  47. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/base.py +0 -0
  48. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/common.py +0 -0
  49. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/models/entities.py +0 -0
  50. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/py.typed +0 -0
  51. {python_kanka-2.5.0 → python_kanka-2.6.1}/src/kanka/types.py +0 -0
  52. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/__init__.py +0 -0
  53. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/README.md +0 -0
  54. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/__init__.py +0 -0
  55. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/base.py +0 -0
  56. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/run_integration_tests.py +0 -0
  57. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/setup_test_env.py +0 -0
  58. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_calendars_integration.py +0 -0
  59. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_calendars_simple.py +0 -0
  60. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_characters_integration.py +0 -0
  61. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_creatures_integration.py +0 -0
  62. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entity_assets_integration.py +0 -0
  63. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entity_images_integration.py +0 -0
  64. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_entity_tags_integration.py +0 -0
  65. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_events_integration.py +0 -0
  66. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_families_integration.py +0 -0
  67. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_gallery_integration.py +0 -0
  68. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_journals_integration.py +0 -0
  69. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_locations_integration.py +0 -0
  70. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_mentions_integration.py +0 -0
  71. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_notes_integration.py +0 -0
  72. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_organisations_integration.py +0 -0
  73. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_posts_integration.py +0 -0
  74. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_quests_integration.py +0 -0
  75. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_races_integration.py +0 -0
  76. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_rate_limiting.py +0 -0
  77. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_search_integration.py +0 -0
  78. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_tag_colour.py +0 -0
  79. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/integration/test_tags_integration.py +0 -0
  80. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/mypy.ini +0 -0
  81. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_client_advanced.py +0 -0
  82. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_entity_models.py +0 -0
  83. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_exceptions.py +0 -0
  84. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_models.py +0 -0
  85. {python_kanka-2.5.0 → python_kanka-2.6.1}/tests/test_models_advanced.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.5.0
2
+ current_version = 2.6.1
3
3
  commit = True
4
4
  tag = True
5
5
  tag_name = v{new_version}
@@ -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@v5
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@v6
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.5.0
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://github.com/ervwalter/python-kanka#readme
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(page: int = 1, limit: int = 15, **filters) → list[dict[str, Any]]
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 `lastSync` Filter
75
+ ### The `last_sync` Parameter
76
76
 
77
- Use the `lastSync` filter with an ISO 8601 timestamp to retrieve only entities modified after a specific time. This is useful for synchronization workflows:
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
- from datetime import datetime, timedelta, timezone
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
- last_sync = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
83
- updated = client.characters.list(lastSync=last_sync, related=True)
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
- | `lastSync` | `str` | Only return entities modified after this ISO 8601 timestamp |
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 `lastSync` filter is useful for synchronization workflows — it returns only entities that have been modified since the given timestamp:
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.5.0"
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://github.com/ervwalter/python-kanka#readme"
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",
@@ -1,3 +1,3 @@
1
1
  """Version information for python-kanka."""
2
2
 
3
- __version__ = "2.5.0"
3
+ __version__ = "2.6.1"
@@ -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, page: int = 1, limit: int = 15, **filters
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
- Pagination information is available after calling entities():
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."""