litestar-pagination 0.2.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.
Files changed (60) hide show
  1. litestar_pagination-0.2.0/.agents/skills/async-python-patterns/SKILL.md +39 -0
  2. litestar_pagination-0.2.0/.agents/skills/async-python-patterns/agents/openai.yaml +7 -0
  3. litestar_pagination-0.2.0/.agents/skills/async-python-patterns/resources/implementation-playbook.md +40 -0
  4. litestar_pagination-0.2.0/.agents/skills/github-actions-pin-refresh/SKILL.md +108 -0
  5. litestar_pagination-0.2.0/.agents/skills/github-actions-pin-refresh/agents/openai.yaml +7 -0
  6. litestar_pagination-0.2.0/.agents/skills/github-actions-pin-refresh/scripts/list_pinned_actions.py +259 -0
  7. litestar_pagination-0.2.0/.agents/skills/python-packaging/SKILL.md +50 -0
  8. litestar_pagination-0.2.0/.agents/skills/python-packaging/agents/openai.yaml +7 -0
  9. litestar_pagination-0.2.0/.agents/skills/python-packaging/resources/implementation-playbook.md +34 -0
  10. litestar_pagination-0.2.0/.agents/skills/python-pro/SKILL.md +50 -0
  11. litestar_pagination-0.2.0/.agents/skills/python-pro/agents/openai.yaml +7 -0
  12. litestar_pagination-0.2.0/.agents/skills/python-testing-patterns/SKILL.md +42 -0
  13. litestar_pagination-0.2.0/.agents/skills/python-testing-patterns/agents/openai.yaml +7 -0
  14. litestar_pagination-0.2.0/.agents/skills/software-architecture/SKILL.md +46 -0
  15. litestar_pagination-0.2.0/.agents/skills/software-architecture/agents/openai.yaml +7 -0
  16. litestar_pagination-0.2.0/.agents/skills/sqlalchemy-orm/SKILL.md +45 -0
  17. litestar_pagination-0.2.0/.agents/skills/sqlalchemy-orm/agents/openai.yaml +7 -0
  18. litestar_pagination-0.2.0/.agents/skills/verification-before-completion/SKILL.md +39 -0
  19. litestar_pagination-0.2.0/.agents/skills/verification-before-completion/agents/openai.yaml +7 -0
  20. litestar_pagination-0.2.0/.editorconfig +21 -0
  21. litestar_pagination-0.2.0/.github/codeql/codeql-config.yml +18 -0
  22. litestar_pagination-0.2.0/.github/dependabot.yml +34 -0
  23. litestar_pagination-0.2.0/.github/workflows/1_test.yml +227 -0
  24. litestar_pagination-0.2.0/.github/workflows/2_bump.yml +129 -0
  25. litestar_pagination-0.2.0/.github/workflows/2_rollback.yml +74 -0
  26. litestar_pagination-0.2.0/.github/workflows/3_release.yml +199 -0
  27. litestar_pagination-0.2.0/.github/workflows/4_codeql.yml +49 -0
  28. litestar_pagination-0.2.0/.github/workflows/5_dependency_review.yml +25 -0
  29. litestar_pagination-0.2.0/.gitignore +181 -0
  30. litestar_pagination-0.2.0/.pre-commit-config.yaml +100 -0
  31. litestar_pagination-0.2.0/.vscode/settings.json +28 -0
  32. litestar_pagination-0.2.0/AGENTS.md +107 -0
  33. litestar_pagination-0.2.0/CHANGELOG.md +7 -0
  34. litestar_pagination-0.2.0/CLAUDE.md +1 -0
  35. litestar_pagination-0.2.0/LICENSE +21 -0
  36. litestar_pagination-0.2.0/PKG-INFO +47 -0
  37. litestar_pagination-0.2.0/PRD.md +504 -0
  38. litestar_pagination-0.2.0/README.md +11 -0
  39. litestar_pagination-0.2.0/TECHSTACK.md +30 -0
  40. litestar_pagination-0.2.0/context7.json +3 -0
  41. litestar_pagination-0.2.0/docs/contributing.md +11 -0
  42. litestar_pagination-0.2.0/docs/index.md +199 -0
  43. litestar_pagination-0.2.0/justfile +66 -0
  44. litestar_pagination-0.2.0/litestar_pagination/__init__.py +7 -0
  45. litestar_pagination-0.2.0/litestar_pagination/cursor.py +64 -0
  46. litestar_pagination-0.2.0/litestar_pagination/ext/__init__.py +1 -0
  47. litestar_pagination-0.2.0/litestar_pagination/ext/sqlalchemy.py +261 -0
  48. litestar_pagination-0.2.0/litestar_pagination/py.typed +0 -0
  49. litestar_pagination-0.2.0/pyproject.toml +179 -0
  50. litestar_pagination-0.2.0/tests/__init__.py +1 -0
  51. litestar_pagination-0.2.0/tests/integration/__init__.py +1 -0
  52. litestar_pagination-0.2.0/tests/integration/test_advanced_alchemy.py +127 -0
  53. litestar_pagination-0.2.0/tests/integration/test_litestar.py +192 -0
  54. litestar_pagination-0.2.0/tests/integration/test_postgresql.py +249 -0
  55. litestar_pagination-0.2.0/tests/integration/test_sqlalchemy.py +410 -0
  56. litestar_pagination-0.2.0/tests/unit/__init__.py +1 -0
  57. litestar_pagination-0.2.0/tests/unit/test_cursor.py +69 -0
  58. litestar_pagination-0.2.0/tests/unit/test_package.py +14 -0
  59. litestar_pagination-0.2.0/uv.lock +1939 -0
  60. litestar_pagination-0.2.0/zensical.toml +88 -0
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: async-python-patterns
3
+ description: Async patterns for litestar-pagination: AsyncSession execution, async cursor pagination, Litestar request-scoped sessions, cancellation, sync/async parity, and pytest-asyncio tests. Use when implementing or reviewing async code in this project.
4
+ ---
5
+
6
+ # Async Python Patterns — litestar-pagination
7
+
8
+ Combine this skill with `python-pro` for async implementation or review.
9
+
10
+ ## Evidence first
11
+
12
+ - Read `AGENTS.md`, `PRD.md`, and `TECHSTACK.md`.
13
+ - Use Context7 for current `AsyncSession`, Litestar DI, Advanced Alchemy session, sqlakeyset async, and pytest-asyncio behavior.
14
+ - Trace the sync path before duplicating it asynchronously; shared pure query construction belongs in one helper.
15
+
16
+ ## Rules
17
+
18
+ - `apaginate()` accepts an existing request-scoped `AsyncSession`; it never creates, commits, closes, or owns that session.
19
+ - Await count and page queries explicitly and preserve their SQLAlchemy exceptions.
20
+ - Do not call sync session methods, blocking I/O, `time.sleep()`, or synchronous database drivers in async code.
21
+ - Keep cursor decoding and query construction synchronous when they are CPU-only and trivial.
22
+ - Re-raise `asyncio.CancelledError` after cleanup; do not convert cancellation into pagination errors.
23
+ - Do not add concurrent count/page execution without evidence: they share a session/transaction and many drivers prohibit simultaneous operations.
24
+ - Keep `paginate()` and `apaginate()` behavior symmetric. Differences require tests and documentation.
25
+
26
+ ## Litestar and Advanced Alchemy
27
+
28
+ - Advanced Alchemy owns engine/session lifecycle; consume its injected session unchanged.
29
+ - Use Litestar dependency injection for `CursorParams`; no pagination plugin or route patching.
30
+ - Request state must not be stored in module globals or mutable singletons.
31
+
32
+ ## Tests
33
+
34
+ - pytest-asyncio runs in auto mode; do not add `@pytest.mark.asyncio` by habit.
35
+ - Test against real async SQLite and PostgreSQL drivers, not `AsyncMock` session behavior.
36
+ - Pair important sync and async cases: first/last page, forward/backward traversal, malformed cursor, count override, and no-total mode.
37
+ - Assert observable page contents and bookmarks, not internal await counts.
38
+
39
+ See `resources/implementation-playbook.md` for the canonical shape.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Async Python Patterns"
3
+ short_description: "Async SQLAlchemy cursor-pagination patterns."
4
+ default_prompt: "Use $async-python-patterns to review or implement async cursor pagination with correct AsyncSession ownership, cancellation, parity, and tests."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,40 @@
1
+ # Async Pagination — Implementation Playbook
2
+
3
+ ## Request-scoped session
4
+
5
+ ```python
6
+ @get("/users")
7
+ async def list_users(
8
+ db_session: AsyncSession,
9
+ pagination: CursorParams,
10
+ ) -> CursorPage[User]:
11
+ statement = select(User).order_by(User.created_at, User.id)
12
+ return await apaginate(db_session, statement, pagination)
13
+ ```
14
+
15
+ The session is injected and owned by Litestar/Advanced Alchemy. `apaginate()` only executes statements.
16
+
17
+ ## Shared query construction
18
+
19
+ Keep statement validation, count-statement construction, cursor decoding, and result normalization in synchronous pure helpers. Sync and async entrypoints should differ only at execution boundaries.
20
+
21
+ ## Do not parallelize one session
22
+
23
+ Do not run count and page statements concurrently on the same `AsyncSession`. Execute them in the documented order so transaction and driver behavior remains predictable.
24
+
25
+ ## Async tests
26
+
27
+ ```python
28
+ import pytest
29
+
30
+
31
+ pytestmark = pytest.mark.integration
32
+
33
+
34
+ async def test_next_page(async_session: AsyncSession) -> None:
35
+ first = await apaginate(async_session, statement, CursorParams(size=2))
36
+ second = await apaginate(async_session, statement, CursorParams(cursor=first.next_page, size=2))
37
+ assert [item.id for item in second.items] == [3, 4]
38
+ ```
39
+
40
+ Use real async database drivers and pytest-asyncio auto mode.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: github-actions-pin-refresh
3
+ description: "Audit `.github` workflows for remote `uses:` entries pinned to full commit SHAs, resolve the newest intended upstream refs, refresh the pinned SHAs, and update trailing version comments when they change. Use when reviewing stale GitHub Actions pins or normalizing `uses: owner/repo@SHA # vX.Y.Z` lines."
4
+ ---
5
+
6
+ # GitHub Actions Pin Refresh
7
+
8
+ Use this skill when changing `.github/workflows/*.yml` or reusable workflows that pin remote actions by full SHA.
9
+
10
+ Use AgentMemory for prior CI/release decisions before changing workflow behavior. CodeGraph is usually unnecessary for pin-only edits; use it only if workflow changes require code impact analysis.
11
+
12
+ ## Goal
13
+
14
+ Keep remote `uses:` lines on full 40-character commit SHAs while moving them to the newest intended upstream version and keeping the trailing `# ...` comment honest.
15
+
16
+ ## Inventory First
17
+
18
+ Run the bundled check before editing:
19
+
20
+ ```bash
21
+ python3 .agents/skills/github-actions-pin-refresh/scripts/list_pinned_actions.py --check
22
+ python3 .agents/skills/github-actions-pin-refresh/scripts/list_pinned_actions.py --format json
23
+ ```
24
+
25
+ `--check` exits nonzero for a mutable remote GitHub-hosted ref. The inventory command lists existing full-SHA pins like:
26
+
27
+ ```yaml
28
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29
+ ```
30
+
31
+ It intentionally ignores local actions (`./...`) and docker actions.
32
+
33
+ ## Update Workflow
34
+
35
+ For each unique `owner/repo[/path]` found under `.github/`:
36
+
37
+ 1. Read the current trailing comment, if any.
38
+ 2. Resolve the target upstream ref:
39
+ - Default: the newest stable release or tag.
40
+ - Ignore prereleases unless the current pin already tracks one or the user explicitly asks for prereleases.
41
+ - If the existing comment is a channel such as `v4`, `v4.1`, `main`, or `master`, preserve that channel unless the user explicitly asks for a cross-channel or cross-major jump.
42
+ 3. Resolve the exact commit SHA for that ref.
43
+ 4. Rewrite the YAML to keep full-SHA pinning:
44
+ - `uses: owner/repo[/path]@<40-char-commit-sha> # <comment>`
45
+ 5. Update the trailing comment when needed:
46
+ - exact-version comments such as `# v6.0.2` should become the new exact tag;
47
+ - channel comments such as `# v6`, `# v6.1`, or `# main` stay channel comments;
48
+ - if comments are used consistently in the file and one pinned action is missing its comment, add the resolved ref comment.
49
+ 6. If the update crosses a major version, skim upstream release notes or changelog before finalizing and adjust the workflow if required (inputs, permissions, Node/runtime expectations, removed flags).
50
+
51
+ ## Resolve Refs Without Guessing
52
+
53
+ Prefer GitHub CLI when it is available.
54
+
55
+ ### Latest Stable Release
56
+
57
+ ```bash
58
+ gh api repos/{owner}/{repo}/releases/latest
59
+ ```
60
+
61
+ Use this when the action publishes stable GitHub releases.
62
+
63
+ ### Exact Tag or Branch Ref
64
+
65
+ ```bash
66
+ gh api repos/{owner}/{repo}/git/ref/tags/{tag}
67
+ gh api repos/{owner}/{repo}/git/ref/heads/{branch}
68
+ ```
69
+
70
+ Annotated tags point to tag objects, not directly to commits. Dereference them:
71
+
72
+ ```bash
73
+ gh api repos/{owner}/{repo}/git/tags/{tag_object_sha}
74
+ ```
75
+
76
+ Use the nested `object.sha` commit as the final pin when the tag is annotated.
77
+
78
+ ### When `releases/latest` Is Unavailable
79
+
80
+ ```bash
81
+ gh api repos/{owner}/{repo}/tags?per_page=100
82
+ ```
83
+
84
+ Pick the newest stable semver tag that matches the intended track. Do not upgrade a stable track to a prerelease.
85
+
86
+ If GitHub CLI is unavailable, fall back to GitHub's web/API surface or `git ls-remote` for exact refs. Still resolve to a commit SHA and keep the workflow pinned to that full SHA.
87
+
88
+ ## Editing Rules
89
+
90
+ - Never replace a full-SHA pin with a mutable tag or branch.
91
+ - Preserve `owner/repo/path` subpaths for nested actions and reusable workflows.
92
+ - Keep one upstream ref per action family consistent across the repo unless a workflow intentionally needs a different version.
93
+ - Do not invent comments that you cannot justify from the resolved upstream ref.
94
+ - If an upstream release changes required permissions or inputs, update the workflow in the same change.
95
+ - Keep YAML formatting and indentation stable so the diff is easy to review.
96
+
97
+ ## Verification
98
+
99
+ - Re-run the bundled `--check` to confirm every remote GitHub-hosted `uses:` line is pinned to a 40-character SHA.
100
+ - Review `rg "uses:" .github -n` and the `.github` diff.
101
+ - For workflow-only pin refreshes, run the workflow-specific checks from `AGENTS.md`:
102
+ ```bash
103
+ python3 .agents/skills/github-actions-pin-refresh/scripts/list_pinned_actions.py --check
104
+ python3 .agents/skills/github-actions-pin-refresh/scripts/list_pinned_actions.py --format text
105
+ rg "uses:" .github -n
106
+ git diff -- .github
107
+ ```
108
+ - If the workflow change modifies job commands, permissions, matrices, release behavior, or anything that changes how the library is built, tested, published, or deployed, also run the relevant targeted project checks. Use the full Python verification block only when the workflow change needs local evidence that the library test/build path still works.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "GitHub Actions Pin Refresh"
3
+ short_description: "Refresh pinned GitHub Action SHAs and version comments."
4
+ default_prompt: "Use $github-actions-pin-refresh to audit .github workflows, update pinned action SHAs to the intended latest upstream refs, and fix trailing version comments."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,259 @@
1
+ """Inventory and validate GitHub-hosted ``uses:`` entries under ``.github``.""" # noqa: INP001
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import re
8
+ import sys
9
+ from dataclasses import asdict, dataclass
10
+ from pathlib import Path
11
+
12
+ FULL_SHA_PATTERN = re.compile(r"[0-9a-fA-F]{40}\Z")
13
+ USES_PATTERN = re.compile(
14
+ r"^\s*(?:-\s*)?uses:\s*"
15
+ r"(?P<slug>[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[^\s@#]+)?)"
16
+ r"@(?P<ref>[^\s#]+)"
17
+ r"(?:\s*#\s*(?P<comment>.*?))?\s*$",
18
+ )
19
+ YAML_SUFFIXES = frozenset({".yml", ".yaml"})
20
+ DEFAULT_REPO_ROOT = Path(__file__).resolve().parents[4]
21
+ DEFAULT_GITHUB_DIR = DEFAULT_REPO_ROOT / ".github"
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class WorkflowUse:
26
+ """A GitHub-hosted ``uses:`` entry from a workflow-related YAML file."""
27
+
28
+ file: str
29
+ line: int
30
+ slug: str
31
+ ref: str
32
+ comment: str | None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class PinnedAction:
37
+ """A remote GitHub Action or reusable workflow pinned to a commit SHA."""
38
+
39
+ file: str
40
+ line: int
41
+ repository: str
42
+ subpath: str | None
43
+ uses_target: str
44
+ sha: str
45
+ comment: str | None
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class UnpinnedAction:
50
+ """A remote GitHub Action or reusable workflow not pinned to a full SHA."""
51
+
52
+ file: str
53
+ line: int
54
+ uses_target: str
55
+ ref: str
56
+ comment: str | None
57
+
58
+
59
+ def build_parser() -> argparse.ArgumentParser:
60
+ """Build the command-line parser.
61
+
62
+ Returns:
63
+ The configured parser.
64
+ """
65
+ parser = argparse.ArgumentParser(
66
+ description="List or validate remote GitHub-hosted uses: entries under .github.",
67
+ )
68
+ parser.add_argument(
69
+ "--repo-root",
70
+ type=Path,
71
+ default=DEFAULT_REPO_ROOT,
72
+ help="Repository root used to relativize file paths.",
73
+ )
74
+ parser.add_argument(
75
+ "--github-dir",
76
+ type=Path,
77
+ default=DEFAULT_GITHUB_DIR,
78
+ help="Directory to scan for workflow YAML files.",
79
+ )
80
+ parser.add_argument(
81
+ "--format",
82
+ choices=("text", "json"),
83
+ default="text",
84
+ help="Output format.",
85
+ )
86
+ parser.add_argument(
87
+ "--check",
88
+ action="store_true",
89
+ help="Exit nonzero when a remote GitHub-hosted uses: entry is not pinned to a full SHA.",
90
+ )
91
+ return parser
92
+
93
+
94
+ def iter_yaml_files(github_dir: Path) -> list[Path]:
95
+ """Return YAML files under a validated GitHub directory.
96
+
97
+ Returns:
98
+ Workflow-related YAML files in deterministic order.
99
+ """
100
+ return sorted(path for path in github_dir.rglob("*") if path.is_file() and path.suffix in YAML_SUFFIXES)
101
+
102
+
103
+ def display_path(path: Path, *, repo_root: Path) -> str:
104
+ """Return a repo-relative display path when possible.
105
+
106
+ Returns:
107
+ A display path.
108
+ """
109
+ try:
110
+ return path.relative_to(repo_root).as_posix()
111
+ except ValueError:
112
+ return path.as_posix()
113
+
114
+
115
+ def parse_workflow_use(line: str, *, file: Path, line_number: int, repo_root: Path) -> WorkflowUse | None:
116
+ """Parse one remote GitHub-hosted ``uses:`` line.
117
+
118
+ Returns:
119
+ The parsed entry, or ``None`` for local, Docker, and unrelated lines.
120
+ """
121
+ match = USES_PATTERN.match(line)
122
+ if match is None:
123
+ return None
124
+
125
+ comment = match.group("comment")
126
+ return WorkflowUse(
127
+ file=display_path(file, repo_root=repo_root),
128
+ line=line_number,
129
+ slug=match.group("slug"),
130
+ ref=match.group("ref"),
131
+ comment=(comment.strip() or None) if comment is not None else None,
132
+ )
133
+
134
+
135
+ def parse_pinned_action(line: str, *, file: Path, line_number: int, repo_root: Path) -> PinnedAction | None:
136
+ """Parse one fully pinned remote GitHub-hosted ``uses:`` line.
137
+
138
+ Returns:
139
+ The parsed pinned entry, or ``None`` when the line is not a full-SHA pin.
140
+ """
141
+ workflow_use = parse_workflow_use(line, file=file, line_number=line_number, repo_root=repo_root)
142
+ if workflow_use is None or FULL_SHA_PATTERN.fullmatch(workflow_use.ref) is None:
143
+ return None
144
+
145
+ owner, repository_name, *subpath_parts = workflow_use.slug.split("/")
146
+ return PinnedAction(
147
+ file=workflow_use.file,
148
+ line=workflow_use.line,
149
+ repository=f"{owner}/{repository_name}",
150
+ subpath="/".join(subpath_parts) or None,
151
+ uses_target=workflow_use.slug,
152
+ sha=workflow_use.ref.lower(),
153
+ comment=workflow_use.comment,
154
+ )
155
+
156
+
157
+ def collect_pinned_actions(*, repo_root: Path, github_dir: Path) -> list[PinnedAction]:
158
+ """Collect full-SHA pins from workflow-related YAML files.
159
+
160
+ Returns:
161
+ Parsed full-SHA pins.
162
+ """
163
+ pinned_actions: list[PinnedAction] = []
164
+ for workflow_file in iter_yaml_files(github_dir):
165
+ for line_number, line in enumerate(workflow_file.read_text(encoding="utf-8").splitlines(), start=1):
166
+ if pinned_action := parse_pinned_action(
167
+ line,
168
+ file=workflow_file,
169
+ line_number=line_number,
170
+ repo_root=repo_root,
171
+ ):
172
+ pinned_actions.append(pinned_action)
173
+ return pinned_actions
174
+
175
+
176
+ def collect_unpinned_actions(*, repo_root: Path, github_dir: Path) -> list[UnpinnedAction]:
177
+ """Collect remote GitHub-hosted entries that are not full-SHA pins.
178
+
179
+ Returns:
180
+ Parsed mutable references.
181
+ """
182
+ unpinned_actions: list[UnpinnedAction] = []
183
+ for workflow_file in iter_yaml_files(github_dir):
184
+ for line_number, line in enumerate(workflow_file.read_text(encoding="utf-8").splitlines(), start=1):
185
+ workflow_use = parse_workflow_use(line, file=workflow_file, line_number=line_number, repo_root=repo_root)
186
+ if workflow_use is not None and FULL_SHA_PATTERN.fullmatch(workflow_use.ref) is None:
187
+ unpinned_actions.append(
188
+ UnpinnedAction(
189
+ file=workflow_use.file,
190
+ line=workflow_use.line,
191
+ uses_target=workflow_use.slug,
192
+ ref=workflow_use.ref,
193
+ comment=workflow_use.comment,
194
+ ),
195
+ )
196
+ return unpinned_actions
197
+
198
+
199
+ def render_text(items: list[PinnedAction] | list[UnpinnedAction]) -> str:
200
+ """Render action items in a concise, human-readable form.
201
+
202
+ Returns:
203
+ Rendered lines, or an empty string for an empty list.
204
+ """
205
+ if not items:
206
+ return ""
207
+
208
+ if isinstance(items[0], PinnedAction):
209
+ lines = ["file:line | target | sha | comment"]
210
+ lines.extend(
211
+ f"{item.file}:{item.line} | {item.uses_target} | {item.sha} | {item.comment or '-'}" for item in items
212
+ )
213
+ else:
214
+ lines = ["file:line | target | mutable ref | comment"]
215
+ lines.extend(
216
+ f"{item.file}:{item.line} | {item.uses_target} | {item.ref} | {item.comment or '-'}" for item in items
217
+ )
218
+ return "\n".join(lines)
219
+
220
+
221
+ def write_items(items: list[PinnedAction] | list[UnpinnedAction], *, output_format: str) -> None:
222
+ """Write action items in the selected format."""
223
+ if output_format == "json":
224
+ json.dump([asdict(item) for item in items], sys.stdout, indent=2, ensure_ascii=False)
225
+ sys.stdout.write("\n")
226
+ return
227
+
228
+ text_output = render_text(items)
229
+ if text_output:
230
+ sys.stdout.write(text_output)
231
+ sys.stdout.write("\n")
232
+
233
+
234
+ def main() -> int:
235
+ """Run the inventory or validation command.
236
+
237
+ Returns:
238
+ A process exit code.
239
+ """
240
+ parser = build_parser()
241
+ args = parser.parse_args()
242
+ repo_root = args.repo_root.resolve()
243
+ github_dir = args.github_dir.resolve()
244
+ if not github_dir.is_dir():
245
+ sys.stderr.write(f"[ERROR] GitHub directory does not exist or is not a directory: {github_dir}\n")
246
+ return 2
247
+
248
+ if args.check:
249
+ unpinned_actions = collect_unpinned_actions(repo_root=repo_root, github_dir=github_dir)
250
+ write_items(unpinned_actions, output_format=args.format)
251
+ return int(bool(unpinned_actions))
252
+
253
+ pinned_actions = collect_pinned_actions(repo_root=repo_root, github_dir=github_dir)
254
+ write_items(pinned_actions, output_format=args.format)
255
+ return 0
256
+
257
+
258
+ if __name__ == "__main__":
259
+ raise SystemExit(main())
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: python-packaging
3
+ description: Packaging guide for litestar-pagination: hatchling, uv, commitizen, optional SQLAlchemy/Advanced Alchemy extras, dependency groups, builds, and releases. Use for pyproject, dependencies, lockfiles, CI packaging, or publishing work.
4
+ ---
5
+
6
+ # Python Packaging — litestar-pagination
7
+
8
+ ## Evidence first
9
+
10
+ - Read `AGENTS.md`, `PRD.md`, `TECHSTACK.md`, and `pyproject.toml`.
11
+ - Use Context7 or official project docs when tool/dependency behavior matters.
12
+ - Treat `pyproject.toml` and `uv.lock` as authoritative.
13
+
14
+ ## Project shape
15
+
16
+ - Distribution: `litestar-pagination`; flat import package: `litestar_pagination`.
17
+ - Build backend: hatchling.
18
+ - Supported Python: `>=3.12.0,<3.15.0`.
19
+ - Base dependency: Litestar.
20
+ - Public extras: `sqlalchemy`, `advanced-alchemy`, and `all`.
21
+ - Dev/docs tools belong in PEP 735 dependency groups, not public extras.
22
+ - `[tool.uv] default-groups = []` keeps installs minimal.
23
+
24
+ ## Dependency rules
25
+
26
+ - Use uv, not ad-hoc pip or manually managed virtualenvs.
27
+ - Use compatible ranges `>=X,<NEXT_MAJOR`; exact resolutions belong in `uv.lock`.
28
+ - SQLAlchemy and sqlakeyset remain optional and must not leak into root imports.
29
+ - The Advanced Alchemy extra includes the SQLAlchemy integration.
30
+ - Update extras, lockfile, docs, deptry configuration, and CI together when dependencies change.
31
+
32
+ ```bash
33
+ uv sync
34
+ uv sync --frozen --all-extras --group dev
35
+ uv lock
36
+ ```
37
+
38
+ ## Version and release
39
+
40
+ - Commitizen uses conventional commits and PEP 621 versioning.
41
+ - Version is mirrored in `pyproject.toml` and `litestar_pagination/__init__.py`.
42
+ - Tags are bare semantic versions without a `v` prefix.
43
+ - Build with `uv build`; publish only when explicitly authorized.
44
+ - Verify wheel contents include `py.typed` and optional imports remain absent from base installation.
45
+
46
+ ## Verification
47
+
48
+ Run the full block in `AGENTS.md`, plus `uv build` for build/release changes. `uv run pip-audit` is best effort when network is available.
49
+
50
+ See `resources/implementation-playbook.md` for common commands.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Python Packaging"
3
+ short_description: "Packaging, uv, dependencies, build, release."
4
+ default_prompt: "Use $python-packaging to modify or review pyproject.toml, uv dependencies, optional extras, build settings, or release tooling."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,34 @@
1
+ # Packaging — Implementation Playbook
2
+
3
+ ## Authoritative files
4
+
5
+ - `pyproject.toml`: metadata, dependencies, tools, build, and versioning.
6
+ - `uv.lock`: exact resolution used by CI.
7
+ - `litestar_pagination/__init__.py`: mirrored package version.
8
+ - `AGENTS.md`: verification commands.
9
+
10
+ ## Common commands
11
+
12
+ ```bash
13
+ uv lock
14
+ just setup
15
+ uv run ruff check --fix .
16
+ uv run ruff format .
17
+ uv run ty check
18
+ uv run deptry .
19
+ just test
20
+ uv build
21
+ ```
22
+
23
+ ## Extras
24
+
25
+ - Base: Litestar dataclass/DI/OpenAPI surface.
26
+ - `sqlalchemy`: SQLAlchemy and sqlakeyset adapter.
27
+ - `advanced-alchemy`: Advanced Alchemy compatibility plus SQLAlchemy adapter.
28
+ - `all`: every public integration.
29
+
30
+ Keep optional imports lazy and test a base-only installation before release.
31
+
32
+ ## Release
33
+
34
+ Commitizen updates `pyproject.toml` and `litestar_pagination/__init__.py`, refreshes `uv.lock`, and creates a bare semantic-version tag. GitHub Actions builds reproducible artifacts and publishes through trusted PyPI OIDC only after the tag test workflow succeeds.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: python-pro
3
+ description: Python 3.12+ coding standards for litestar-pagination: modern typing, dataclass contracts, optional integration boundaries, Ruff ALL rules, cursor validation, and public API conventions. Use proactively when writing or reviewing Python code in this project.
4
+ ---
5
+
6
+ # Python Pro — litestar-pagination
7
+
8
+ Use this skill for all Python implementation and review work in this repository.
9
+
10
+ ## Evidence first
11
+
12
+ - Read `AGENTS.md`, `PRD.md`, `TECHSTACK.md`, and relevant code before editing.
13
+ - Use CodeGraph for flows and impact when `.codegraph/` exists.
14
+ - Use Context7 for current Litestar, Advanced Alchemy, SQLAlchemy, sqlakeyset, or pytest APIs.
15
+ - Do not infer behavior from `fastapi-pagination`; inspect it only as prior art and preserve this project's PRD.
16
+
17
+ ## Python and typing
18
+
19
+ - Support Python 3.12–3.14 and use built-in generics, `X | None`, PEP 695 type aliases/generics, `Self`, and `override` where useful.
20
+ - Fully annotate every public function and method. Keep sync `paginate()` and async `apaginate()` return types precise.
21
+ - Use `collections.abc` for `Sequence`, `Callable`, and iterator interfaces.
22
+ - Prefer narrow helpers and explicit type guards over repeated `cast()`.
23
+ - Put import-only dependencies behind `TYPE_CHECKING` when that preserves optional-import boundaries.
24
+
25
+ ## Public contracts
26
+
27
+ - Use standard-library dataclasses for `CursorParams` and `CursorPage`; do not add Pydantic.
28
+ - Keep root imports lightweight and stable. SQLAlchemy APIs live in `litestar_pagination.ext.sqlalchemy`.
29
+ - Do not import optional SQLAlchemy or Advanced Alchemy dependencies from the package root.
30
+ - Public query names, response fields, defaults, and cursor semantics come from `PRD.md`.
31
+
32
+ ## Error handling and trust boundaries
33
+
34
+ - Catch the narrowest exception and chain translated errors with `raise ... from error`.
35
+ - Preserve SQLAlchemy execution errors; do not relabel database failures as client input failures.
36
+ - Invalid external cursors map to Litestar HTTP 400. Missing ordering and unsupported statement shapes are developer errors.
37
+ - Never place decoded bookmark values or sensitive query data in logs or error responses.
38
+ - Base64 is encoding, not encryption or signing; do not imply otherwise.
39
+
40
+ ## Ruff and style
41
+
42
+ - Ruff uses `ALL`, preview mode, Google docstrings, 120-character lines, and Python 3.12 syntax.
43
+ - Use `msg = "..."; raise Error(msg)` when required by Ruff's exception-message rules.
44
+ - Use stdlib → third-party → local import order.
45
+ - Every `# pragma: no cover` must include a reason.
46
+ - Prefer deletion and direct code over factories, registries, protocols, and abstractions without two real consumers.
47
+
48
+ ## Verification
49
+
50
+ Before completion claims, use `verification-before-completion`. For Python changes run the full verification block in `AGENTS.md`.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Python Pro"
3
+ short_description: "Python 3.12+ standards for litestar-pagination."
4
+ default_prompt: "Use $python-pro to write or review Python code in this project with modern typing, Ruff ALL compliance, and project conventions."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: python-testing-patterns
3
+ description: Test-writing guide for litestar-pagination. Use for pytest tests covering cursor codecs, keyset traversal, SQLAlchemy sync/async behavior, Litestar DI/OpenAPI/DTO integration, Advanced Alchemy sessions, and supported databases.
4
+ ---
5
+
6
+ # Python Testing Patterns — litestar-pagination
7
+
8
+ Combine this skill with `python-pro`; also use `async-python-patterns` for async tests.
9
+
10
+ ## Evidence first
11
+
12
+ - Read `AGENTS.md`, `PRD.md`, `TECHSTACK.md`, and the public API under test.
13
+ - Use Context7 when assertions depend on current Litestar, SQLAlchemy, sqlakeyset, Advanced Alchemy, or pytest behavior.
14
+ - Use CodeGraph for test-impact discovery when available.
15
+
16
+ ## Test shape
17
+
18
+ - Use markers: `unit`, `integration`, and `e2e`.
19
+ - pytest-asyncio uses auto mode and a session-scoped loop.
20
+ - Prefer behavior assertions through public imports and functions.
21
+ - Keep fixtures narrow and data deterministic. Avoid helper layers that hide ordering or bookmark inputs.
22
+ - Do not mock SQLAlchemy/sqlakeyset semantics. Use real SQLite/PostgreSQL engines and sessions.
23
+
24
+ ## Required behavior coverage
25
+
26
+ - Codec round trips and malformed cursor HTTP 400 behavior.
27
+ - Size defaults and boundaries through Litestar DI.
28
+ - Forward and backward traversal without duplicates or omissions on a stable dataset.
29
+ - Ascending, descending, and compound ordering with duplicate leading values and a unique tie-breaker.
30
+ - Missing ordering and unsupported select shapes as developer errors.
31
+ - Entity result unwrapping and uniquification.
32
+ - Default, custom, and disabled total counting.
33
+ - Sync/async parity on SQLite and PostgreSQL.
34
+ - OpenAPI, dataclass serialization, and `SQLAlchemyDTO` transformation inside `items`.
35
+ - Sessions injected by Advanced Alchemy without an additional plugin.
36
+
37
+ ## Quality
38
+
39
+ - Tests should fail for one clear reason and assert external behavior before implementation details.
40
+ - Use parametrization for small matrices; keep dialect-specific expectations explicit.
41
+ - Do not add tautological tests or branches solely to satisfy coverage.
42
+ - Run the verification block from `AGENTS.md` for library/test changes.
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Python Testing Patterns"
3
+ short_description: "Pytest patterns for cursor pagination."
4
+ default_prompt: "Use $python-testing-patterns to add or review behavior-focused cursor, SQLAlchemy, Litestar, and Advanced Alchemy tests."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true