msw-io 1.0.2__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 (88) hide show
  1. msw_io-1.0.2/.copier-answers.yml +15 -0
  2. msw_io-1.0.2/.forgejo/workflows/ci.yml +77 -0
  3. msw_io-1.0.2/.forgejo/workflows/docs.yml +33 -0
  4. msw_io-1.0.2/.forgejo/workflows/pr-review.yml +105 -0
  5. msw_io-1.0.2/.forgejo/workflows/release.yml +64 -0
  6. msw_io-1.0.2/.githooks/fix_commit_msg.py +52 -0
  7. msw_io-1.0.2/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
  8. msw_io-1.0.2/.github/ISSUE_TEMPLATE/feature_request.md +21 -0
  9. msw_io-1.0.2/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  10. msw_io-1.0.2/.github/workflows/ci.yml +72 -0
  11. msw_io-1.0.2/.github/workflows/docs.yml +31 -0
  12. msw_io-1.0.2/.github/workflows/pr-review.yml +96 -0
  13. msw_io-1.0.2/.github/workflows/release.yml +73 -0
  14. msw_io-1.0.2/.gitignore +72 -0
  15. msw_io-1.0.2/.gitleaks.toml +6 -0
  16. msw_io-1.0.2/.gitlint +2 -0
  17. msw_io-1.0.2/.pre-commit-config.yaml +49 -0
  18. msw_io-1.0.2/CITATION.cff +11 -0
  19. msw_io-1.0.2/CODE_OF_CONDUCT.md +31 -0
  20. msw_io-1.0.2/CONTRIBUTING.md +29 -0
  21. msw_io-1.0.2/LICENSE +0 -0
  22. msw_io-1.0.2/PKG-INFO +80 -0
  23. msw_io-1.0.2/README.md +54 -0
  24. msw_io-1.0.2/VERSION +1 -0
  25. msw_io-1.0.2/docs/api.md +65 -0
  26. msw_io-1.0.2/docs/concepts.md +91 -0
  27. msw_io-1.0.2/docs/getting_started.md +92 -0
  28. msw_io-1.0.2/docs/index.md +32 -0
  29. msw_io-1.0.2/mkdocs.yml +44 -0
  30. msw_io-1.0.2/pyproject.toml +96 -0
  31. msw_io-1.0.2/src/murineshiftwork/_version.py +24 -0
  32. msw_io-1.0.2/src/murineshiftwork/io/__init__.py +134 -0
  33. msw_io-1.0.2/src/murineshiftwork/namespace/__init__.py +16 -0
  34. msw_io-1.0.2/src/murineshiftwork/namespace/manifest.py +163 -0
  35. msw_io-1.0.2/src/murineshiftwork/namespace/msw_files.py +57 -0
  36. msw_io-1.0.2/src/murineshiftwork/namespace/namespace.msw.yaml +25 -0
  37. msw_io-1.0.2/src/murineshiftwork/namespace/paths.py +219 -0
  38. msw_io-1.0.2/src/murineshiftwork/namespace/spec.py +5 -0
  39. msw_io-1.0.2/src/murineshiftwork/readers/__init__.py +5 -0
  40. msw_io-1.0.2/src/murineshiftwork/readers/alignment.py +325 -0
  41. msw_io-1.0.2/src/murineshiftwork/readers/batch.py +181 -0
  42. msw_io-1.0.2/src/murineshiftwork/readers/files.py +108 -0
  43. msw_io-1.0.2/src/murineshiftwork/readers/models.py +105 -0
  44. msw_io-1.0.2/src/murineshiftwork/readers/namespace.py +143 -0
  45. msw_io-1.0.2/src/murineshiftwork/readers/session.py +221 -0
  46. msw_io-1.0.2/src/murineshiftwork/readers/validate.py +93 -0
  47. msw_io-1.0.2/tests/conftest.py +1 -0
  48. msw_io-1.0.2/tests/data/fixture_fixedsubjects/_test_subject__20260518_121602_710824__probabilistic_switching_fixedsubjects.msw.csv +1164 -0
  49. msw_io-1.0.2/tests/data/fixture_fixedsubjects/_test_subject__20260518_121602_710824__probabilistic_switching_fixedsubjects.msw.df.jsonl +9 -0
  50. msw_io-1.0.2/tests/data/fixture_fixedsubjects/_test_subject__20260518_121602_710824__probabilistic_switching_fixedsubjects.msw.session.yaml +144 -0
  51. msw_io-1.0.2/tests/data/fixture_jsonl/subject001__20260508_172956_258756__probabilistic_switching_fixedsubjects.msw.csv +682 -0
  52. msw_io-1.0.2/tests/data/fixture_jsonl/subject001__20260508_172956_258756__probabilistic_switching_fixedsubjects.msw.df.jsonl +3 -0
  53. msw_io-1.0.2/tests/data/fixture_jsonl/subject001__20260508_172956_258756__probabilistic_switching_fixedsubjects.msw.settings.process.json +1639 -0
  54. msw_io-1.0.2/tests/data/fixture_jsonl/subject001__20260508_172956_258756__probabilistic_switching_fixedsubjects.msw.settings.task.json +173 -0
  55. msw_io-1.0.2/tests/data/fixture_legacy/subject003__20210426_183409__probabilistic_switching/subject003__20210426_183409__probabilistic_switching.pkl +0 -0
  56. msw_io-1.0.2/tests/data/fixture_legacy/subject003__20210426_183409__probabilistic_switching/subject003__20210426_183409__probabilistic_switching.task_settings.py +86 -0
  57. msw_io-1.0.2/tests/data/fixture_optotagging/_test_subject__20260527_133053_901389__optotagging/_test_subject__20260527_133053_901389__optotagging.msw.session.yaml +20 -0
  58. msw_io-1.0.2/tests/data/fixture_optotagging/_test_subject__20260527_133053_901389__optotagging/_test_subject__20260527_133053_901389__optotagging_power_ramp_1mw.msw.df.jsonl +2 -0
  59. msw_io-1.0.2/tests/data/fixture_optotagging/_test_subject__20260527_133053_901389__optotagging/_test_subject__20260527_133053_901389__optotagging_power_ramp_2mw.msw.csv +19 -0
  60. msw_io-1.0.2/tests/data/fixture_optotagging/_test_subject__20260527_133053_901389__optotagging/session_manifest.yaml +14 -0
  61. msw_io-1.0.2/tests/data/fixture_pkl/subject002__20260429_153653__probabilistic_switching_fixedsubjects.msw.csv +124 -0
  62. msw_io-1.0.2/tests/data/fixture_pkl/subject002__20260429_153653__probabilistic_switching_fixedsubjects.msw.df.pkl +0 -0
  63. msw_io-1.0.2/tests/data/fixture_pkl/subject002__20260429_153653__probabilistic_switching_fixedsubjects.msw.settings.process.json +1435 -0
  64. msw_io-1.0.2/tests/data/fixture_pkl/subject002__20260429_153653__probabilistic_switching_fixedsubjects.msw.settings.task.json +165 -0
  65. msw_io-1.0.2/tests/data/fixture_sequence/_test_subject__20260520_114516_534465__sequence.msw.csv +2460 -0
  66. msw_io-1.0.2/tests/data/fixture_sequence/_test_subject__20260520_114516_534465__sequence.msw.df.jsonl +66 -0
  67. msw_io-1.0.2/tests/data/fixture_sequence/_test_subject__20260520_114516_534465__sequence.msw.session.yaml +67 -0
  68. msw_io-1.0.2/tests/data/fixture_v2/_test_subject__20260516_130611_739182__probabilistic_switching_fixedsubjects.msw.csv +3560 -0
  69. msw_io-1.0.2/tests/data/fixture_v2/_test_subject__20260516_130611_739182__probabilistic_switching_fixedsubjects.msw.df.jsonl +23 -0
  70. msw_io-1.0.2/tests/data/fixture_v2/_test_subject__20260516_130611_739182__probabilistic_switching_fixedsubjects.msw.session.yaml +184 -0
  71. msw_io-1.0.2/tests/data/namespace.v1.yaml +21 -0
  72. msw_io-1.0.2/tests/data/namespace.v2.yaml +27 -0
  73. msw_io-1.0.2/tests/data/namespace.v3.yaml +26 -0
  74. msw_io-1.0.2/tests/test_ephys_barcode_alignment.py +269 -0
  75. msw_io-1.0.2/tests/test_msw_files.py +171 -0
  76. msw_io-1.0.2/tests/test_namespace.py +234 -0
  77. msw_io-1.0.2/tests/test_namespace_builder.py +202 -0
  78. msw_io-1.0.2/tests/test_namespace_readiness.py +23 -0
  79. msw_io-1.0.2/tests/test_rce_barcode_alignment.py +165 -0
  80. msw_io-1.0.2/tests/test_reader_batch.py +335 -0
  81. msw_io-1.0.2/tests/test_reader_dispatch.py +139 -0
  82. msw_io-1.0.2/tests/test_reader_fixtures.py +402 -0
  83. msw_io-1.0.2/tests/test_reader_interface_parity.py +269 -0
  84. msw_io-1.0.2/tests/test_reader_session_format.py +144 -0
  85. msw_io-1.0.2/tests/test_reader_task_fixtures.py +263 -0
  86. msw_io-1.0.2/tests/test_reader_v2.py +102 -0
  87. msw_io-1.0.2/tests/test_readers_session.py +78 -0
  88. msw_io-1.0.2/tests/test_readers_validate.py +69 -0
@@ -0,0 +1,15 @@
1
+ # Changes here will be overwritten by Copier
2
+ _commit: v0.8.5
3
+ _src_path: gh:larsrollik/templatepy
4
+ author_email: lars@rollik.me
5
+ author_name: Lars B. Rollik
6
+ github_repo: msw-io
7
+ github_username: murineshiftwork
8
+ license_type: noncommercial
9
+ private_repo_deps: false
10
+ project_description: 'Murine Shift Work session data IO: file codec, namespace utilities, and session readers.'
11
+ project_name: msw-io
12
+ project_slug: msw_io
13
+ python_requires: '3.10'
14
+ year: '2026'
15
+ test_on_windows: false
@@ -0,0 +1,77 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags-ignore: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ lint:
15
+ name: Lint
16
+ runs-on: ubuntu-latest
17
+ if: "!startsWith(github.event.head_commit.message, 'bump:')"
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - uses: astral-sh/setup-uv@v7
24
+ with:
25
+ python-version: "3.12"
26
+
27
+
28
+ - name: Install dependencies
29
+ run: uv sync --extra dev
30
+
31
+ - name: Run pre-commit (ruff + mypy + commitizen + gitleaks)
32
+ run: uv run pre-commit run --all-files --show-diff-on-failure
33
+
34
+ test:
35
+ name: Test (Python ${{ matrix.python-version }})
36
+ runs-on: ubuntu-latest
37
+ if: github.event_name == 'pull_request'
38
+ strategy:
39
+ fail-fast: false
40
+ matrix:
41
+
42
+
43
+ python-version: ["3.10", "3.13"]
44
+
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ with:
48
+ fetch-depth: 0
49
+
50
+ - uses: astral-sh/setup-uv@v7
51
+ with:
52
+ python-version: ${{ matrix.python-version }}
53
+
54
+
55
+ - name: Install dependencies
56
+ run: uv sync --extra dev
57
+
58
+ - name: Run tests
59
+ run: uv run pytest
60
+
61
+ # Aggregate gate — add as required status check in branch protection
62
+ ci:
63
+ name: CI
64
+ runs-on: ubuntu-latest
65
+ needs: [lint, test]
66
+ if: always()
67
+ steps:
68
+ - name: Check required jobs
69
+ run: |
70
+
71
+ if [[ "${{ needs.lint.result }}" != "success" && "${{ needs.lint.result }}" != "skipped" ]]; then
72
+ echo "lint failed" && exit 1
73
+ fi
74
+ if [[ "${{ needs.test.result }}" != "success" && "${{ needs.test.result }}" != "skipped" ]]; then
75
+ echo "test failed" && exit 1
76
+ fi
77
+ echo "All required checks passed."
@@ -0,0 +1,33 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - ".forgejo/workflows/docs.yml"
10
+ - "src/**/*.py"
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: write
15
+
16
+ jobs:
17
+ deploy:
18
+ name: Deploy docs to Pages
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0
24
+
25
+ - uses: astral-sh/setup-uv@v7
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install dependencies
30
+ run: uv sync --extra docs
31
+
32
+ - name: Deploy
33
+ run: uv run mkdocs gh-deploy --force
@@ -0,0 +1,105 @@
1
+ name: PR Review (optional LLM)
2
+
3
+ # Optional AI-assisted pull request review.
4
+ # Uncomment ONE of the sections below and configure the required secret.
5
+ #
6
+ # Option A: Claude API requires ANTHROPIC_API_KEY secret.
7
+ # Option B: Ollama requires a self-hosted runner with Ollama on localhost:11434.
8
+ #
9
+ # This workflow has a no-op placeholder job so it stays valid while
10
+ # both options remain commented out.
11
+
12
+ on:
13
+ pull_request:
14
+ types: [opened, synchronize, ready_for_review]
15
+
16
+ jobs:
17
+
18
+ # ── Option A: Claude API (Anthropic) ─────────────────────────────────────
19
+ # Requires secret: ANTHROPIC_API_KEY
20
+ #
21
+ # llm-review-claude:
22
+ # name: Claude PR review
23
+ # runs-on: ubuntu-latest
24
+ # steps:
25
+ # - uses: actions/checkout@v4
26
+ # with:
27
+ # fetch-depth: 0
28
+ #
29
+ # - name: Generate diff
30
+ # id: diff
31
+ # run: |
32
+ # DIFF=$(git diff origin/main...HEAD -- '*.py' | head -c 8000)
33
+ # echo "content<<EOF" >> "$GITHUB_OUTPUT"
34
+ # echo "$DIFF" >> "$GITHUB_OUTPUT"
35
+ # echo "EOF" >> "$GITHUB_OUTPUT"
36
+ #
37
+ # - name: Review with Claude
38
+ # env:
39
+ # ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
40
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41
+ # PR_NUMBER: ${{ github.event.pull_request.number }}
42
+ # run: |
43
+ # if [ -z "$ANTHROPIC_API_KEY" ]; then
44
+ # echo "ANTHROPIC_API_KEY not set — skipping."
45
+ # exit 0
46
+ # fi
47
+ # REVIEW=$(curl -s https://api.anthropic.com/v1/messages \
48
+ # -H "x-api-key: $ANTHROPIC_API_KEY" \
49
+ # -H "anthropic-version: 2023-06-01" \
50
+ # -H "content-type: application/json" \
51
+ # -d "{
52
+ # \"model\": \"claude-sonnet-4-6\",
53
+ # \"max_tokens\": 1024,
54
+ # \"messages\": [{
55
+ # \"role\": \"user\",
56
+ # \"content\": \"Review this Python PR diff for bugs, style issues, and improvements. Be concise.\n\n${{ steps.diff.outputs.content }}\"
57
+ # }]
58
+ # }" | python3 -c "import sys,json; print(json.load(sys.stdin)['content'][0]['text'])")
59
+ # curl -s -X POST \
60
+ # "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
61
+ # -H "Authorization: token ${GITHUB_TOKEN}" \
62
+ # -H "Content-Type: application/json" \
63
+ # -d "{\"body\": \"## Claude PR Review\n\n${REVIEW}\"}"
64
+
65
+ # ── Option B: Local Ollama (self-hosted runner) ───────────────────────────
66
+ # Requires: a self-hosted runner with Ollama running on localhost:11434
67
+ #
68
+ # llm-review-ollama:
69
+ # name: Ollama PR review
70
+ # runs-on: self-hosted
71
+ # steps:
72
+ # - uses: actions/checkout@v4
73
+ # with:
74
+ # fetch-depth: 0
75
+ #
76
+ # - name: Generate diff
77
+ # id: diff
78
+ # run: |
79
+ # DIFF=$(git diff origin/main...HEAD -- '*.py' | head -c 4000)
80
+ # echo "content<<EOF" >> "$GITHUB_OUTPUT"
81
+ # echo "$DIFF" >> "$GITHUB_OUTPUT"
82
+ # echo "EOF" >> "$GITHUB_OUTPUT"
83
+ #
84
+ # - name: Review with Ollama
85
+ # env:
86
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87
+ # PR_NUMBER: ${{ github.event.pull_request.number }}
88
+ # OLLAMA_MODEL: "llama3.2"
89
+ # run: |
90
+ # PROMPT="Review this Python PR diff for bugs, style issues, and improvements. Be concise.\n\n${{ steps.diff.outputs.content }}"
91
+ # REVIEW=$(curl -s http://localhost:11434/api/generate \
92
+ # -d "{\"model\": \"${OLLAMA_MODEL}\", \"prompt\": \"${PROMPT}\", \"stream\": false}" \
93
+ # | python3 -c "import sys,json; print(json.load(sys.stdin)['response'])")
94
+ # curl -s -X POST \
95
+ # "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
96
+ # -H "Authorization: token ${GITHUB_TOKEN}" \
97
+ # -H "Content-Type: application/json" \
98
+ # -d "{\"body\": \"## Ollama PR Review (${OLLAMA_MODEL})\n\n${REVIEW}\"}"
99
+
100
+ # Placeholder — remove once an option above is uncommented
101
+ no-op:
102
+ name: LLM review (not configured)
103
+ runs-on: ubuntu-latest
104
+ steps:
105
+ - run: echo "LLM PR review not configured. See .forgejo/workflows/pr-review.yml."
@@ -0,0 +1,64 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ name: Bump, build, publish
14
+ runs-on: ubuntu-latest
15
+ if: "!startsWith(github.event.head_commit.message, 'bump:')"
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Configure git identity
22
+ run: |
23
+ git config user.name "forgejo-actions[bot]"
24
+ git config user.email "forgejo-actions[bot]@noreply"
25
+
26
+ - uses: astral-sh/setup-uv@v7
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Bump version
31
+ id: bump
32
+ run: |
33
+ uvx --from commitizen cz bump --yes || EXIT=$?
34
+ if [ "${EXIT:-0}" -eq 21 ] || [ "${EXIT:-0}" -eq 6 ]; then
35
+ echo "No bumpable commits since last tag — skipping."
36
+ echo "skipped=true" >> "$GITHUB_OUTPUT"
37
+ exit 0
38
+ fi
39
+ [ "${EXIT:-0}" -eq 0 ] || exit $EXIT
40
+ VERSION=$(cat VERSION)
41
+ git push origin HEAD:main
42
+ git push origin "v${VERSION}"
43
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
44
+
45
+ - name: Build
46
+ if: steps.bump.outputs.skipped != 'true'
47
+ run: uv build
48
+
49
+ - name: Create Forgejo release
50
+ if: steps.bump.outputs.skipped != 'true'
51
+ env:
52
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53
+ VERSION: ${{ steps.bump.outputs.version }}
54
+ run: |
55
+ curl -s -X POST "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
56
+ -H "Authorization: token ${GITHUB_TOKEN}" \
57
+ -H "Content-Type: application/json" \
58
+ -d "{\"tag_name\": \"v${VERSION}\", \"name\": \"v${VERSION}\", \"body\": \"Release v${VERSION}\"}"
59
+
60
+ - name: Publish to PyPI
61
+ if: steps.bump.outputs.skipped != 'true'
62
+ env:
63
+ UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }}
64
+ run: uv publish
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """Auto-fix mechanical commit message formatting issues before linting."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ def fix(path: Path) -> None:
9
+ text = path.read_text()
10
+ lines = text.splitlines(keepends=True)
11
+
12
+ fixed = []
13
+ for i, line in enumerate(lines):
14
+ if line.startswith("#"):
15
+ fixed.append(line)
16
+ continue
17
+
18
+ # Strip trailing whitespace on every line
19
+ line = line.rstrip() + "\n"
20
+
21
+ # Strip leading whitespace from subject line (first content line)
22
+ if i == 0 or all(ln.startswith("#") for ln in lines[:i]):
23
+ line = line.lstrip()
24
+
25
+ # Convert hard tabs to four spaces
26
+ line = line.replace("\t", " ")
27
+
28
+ fixed.append(line)
29
+
30
+ # Ensure blank line between subject and body if body is present
31
+ first = next((i for i, ln in enumerate(fixed) if not ln.startswith("#")), None)
32
+ if first is not None and first + 1 < len(fixed):
33
+ next_content = next(
34
+ (
35
+ i
36
+ for i, ln in enumerate(fixed[first + 1 :], first + 1)
37
+ if not ln.startswith("#")
38
+ ),
39
+ None,
40
+ )
41
+ if (
42
+ next_content is not None
43
+ and next_content == first + 1
44
+ and fixed[first + 1].strip()
45
+ ):
46
+ fixed.insert(first + 1, "\n")
47
+
48
+ path.write_text("".join(fixed))
49
+
50
+
51
+ if __name__ == "__main__":
52
+ fix(Path(sys.argv[1]))
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something isn't working
4
+ labels: bug
5
+ ---
6
+
7
+ ## Description
8
+
9
+ _Clear description of the bug._
10
+
11
+ ## Steps to reproduce
12
+
13
+ 1. ...
14
+ 2. ...
15
+
16
+ ## Expected behaviour
17
+
18
+ _What should happen._
19
+
20
+ ## Actual behaviour
21
+
22
+ _What actually happens._
23
+
24
+ ## Environment
25
+
26
+ - OS:
27
+ - Python version:
28
+ - Package version:
29
+
30
+ ## Additional context
31
+
32
+ _Logs, screenshots, related issues._
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea or improvement
4
+ labels: enhancement
5
+ ---
6
+
7
+ ## Problem
8
+
9
+ _What problem does this solve? What use case does it enable?_
10
+
11
+ ## Proposed solution
12
+
13
+ _Describe what you'd like to see._
14
+
15
+ ## Alternatives considered
16
+
17
+ _Other approaches you've thought about._
18
+
19
+ ## Additional context
20
+
21
+ _Anything else relevant._
@@ -0,0 +1,18 @@
1
+ ## Summary
2
+
3
+ _What does this PR do? Link to any relevant issue with `closes #N`._
4
+
5
+ ## Type
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Refactor / cleanup
10
+ - [ ] Docs / chore
11
+
12
+ ## Checklist
13
+
14
+ - [ ] Branch created from `main`
15
+ - [ ] Commits follow Conventional Commits (`cz commit`)
16
+ - [ ] Version bumped with `cz bump` and pushed with `--follow-tags` (if releasing)
17
+ - [ ] Tests added or updated
18
+ - [ ] `uv run pre-commit run --all-files` passes locally
@@ -0,0 +1,72 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ tags-ignore: ["v*"]
7
+ pull_request:
8
+ branches: [main]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ lint:
15
+ name: Lint
16
+ runs-on: ubuntu-latest
17
+ if: "!startsWith(github.event.head_commit.message, 'bump:')"
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - uses: astral-sh/setup-uv@v7
24
+ with:
25
+ python-version: "3.12"
26
+
27
+ - name: Install dependencies
28
+ run: uv sync --extra dev
29
+
30
+ - name: Run pre-commit (ruff + mypy + commitizen + gitleaks)
31
+ run: uv run pre-commit run --all-files --show-diff-on-failure
32
+
33
+ test:
34
+ name: Test (Python ${{ matrix.python-version }} · ${{ matrix.os }})
35
+ runs-on: ${{ matrix.os }}
36
+ if: github.event_name == 'pull_request'
37
+ strategy:
38
+ fail-fast: false
39
+ matrix:
40
+ os: [ubuntu-latest]
41
+ python-version: ["3.11", "3.13"]
42
+
43
+ steps:
44
+ - uses: actions/checkout@v6
45
+ with:
46
+ fetch-depth: 0
47
+
48
+ - uses: astral-sh/setup-uv@v7
49
+ with:
50
+ python-version: ${{ matrix.python-version }}
51
+
52
+ - name: Install dependencies
53
+ run: uv sync --extra dev
54
+
55
+ - name: Run tests
56
+ run: uv run pytest
57
+
58
+ ci:
59
+ name: CI
60
+ runs-on: ubuntu-latest
61
+ needs: [lint, test]
62
+ if: always()
63
+ steps:
64
+ - name: Check required jobs
65
+ run: |
66
+ if [[ "${{ needs.lint.result }}" != "success" && "${{ needs.lint.result }}" != "skipped" ]]; then
67
+ echo "lint failed" && exit 1
68
+ fi
69
+ if [[ "${{ needs.test.result }}" != "success" && "${{ needs.test.result }}" != "skipped" ]]; then
70
+ echo "test failed" && exit 1
71
+ fi
72
+ echo "All required checks passed."
@@ -0,0 +1,31 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - "src/**/*.py"
10
+
11
+ permissions:
12
+ contents: write
13
+
14
+ jobs:
15
+ deploy:
16
+ name: Deploy docs to GitHub Pages
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - uses: astral-sh/setup-uv@v7
24
+ with:
25
+ python-version: "3.12"
26
+
27
+ - name: Install dependencies
28
+ run: uv sync --extra docs
29
+
30
+ - name: Deploy
31
+ run: uv run mkdocs gh-deploy --force
@@ -0,0 +1,96 @@
1
+ name: PR Review (optional LLM)
2
+
3
+ # Optional AI-assisted pull request review.
4
+ # Uncomment ONE of the sections below and configure the required secret.
5
+ #
6
+ # This workflow has a no-op placeholder job so it stays valid while
7
+ # both options remain commented out.
8
+
9
+ on:
10
+ pull_request:
11
+ types: [opened, synchronize, ready_for_review]
12
+
13
+ jobs:
14
+
15
+ # ── Option A: Claude API (Anthropic) ─────────────────────────────────────
16
+ # Requires secret: ANTHROPIC_API_KEY
17
+ # Docs: https://docs.anthropic.com/en/api/getting-started
18
+ #
19
+ # llm-review-claude:
20
+ # name: Claude PR review
21
+ # runs-on: ubuntu-latest
22
+ # steps:
23
+ # - uses: actions/checkout@v6
24
+ # with:
25
+ # fetch-depth: 0
26
+ #
27
+ # - name: Generate diff
28
+ # id: diff
29
+ # run: |
30
+ # DIFF=$(git diff origin/main...HEAD -- '*.py' | head -c 8000)
31
+ # echo "content<<EOF" >> "$GITHUB_OUTPUT"
32
+ # echo "$DIFF" >> "$GITHUB_OUTPUT"
33
+ # echo "EOF" >> "$GITHUB_OUTPUT"
34
+ #
35
+ # - name: Review with Claude
36
+ # env:
37
+ # ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
38
+ # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39
+ # PR_NUMBER: ${{ github.event.pull_request.number }}
40
+ # run: |
41
+ # if [ -z "$ANTHROPIC_API_KEY" ]; then
42
+ # echo "ANTHROPIC_API_KEY not set — skipping."
43
+ # exit 0
44
+ # fi
45
+ # REVIEW=$(curl -s https://api.anthropic.com/v1/messages \
46
+ # -H "x-api-key: $ANTHROPIC_API_KEY" \
47
+ # -H "anthropic-version: 2023-06-01" \
48
+ # -H "content-type: application/json" \
49
+ # -d "{
50
+ # \"model\": \"claude-sonnet-4-6\",
51
+ # \"max_tokens\": 1024,
52
+ # \"messages\": [{
53
+ # \"role\": \"user\",
54
+ # \"content\": \"Review this Python PR diff for bugs, style issues, and improvements. Be concise.\n\n${{ steps.diff.outputs.content }}\"
55
+ # }]
56
+ # }" | python3 -c "import sys,json; print(json.load(sys.stdin)['content'][0]['text'])")
57
+ # gh pr comment "$PR_NUMBER" --body "## Claude PR Review\n\n${REVIEW}"
58
+
59
+ # ── Option B: Local Ollama (self-hosted runner) ───────────────────────────
60
+ # Requires: a self-hosted runner with Ollama running on localhost:11434
61
+ # Docs: https://ollama.com / https://docs.github.com/en/actions/hosting-your-own-runners
62
+ #
63
+ # llm-review-ollama:
64
+ # name: Ollama PR review
65
+ # runs-on: self-hosted
66
+ # steps:
67
+ # - uses: actions/checkout@v6
68
+ # with:
69
+ # fetch-depth: 0
70
+ #
71
+ # - name: Generate diff
72
+ # id: diff
73
+ # run: |
74
+ # DIFF=$(git diff origin/main...HEAD -- '*.py' | head -c 4000)
75
+ # echo "content<<EOF" >> "$GITHUB_OUTPUT"
76
+ # echo "$DIFF" >> "$GITHUB_OUTPUT"
77
+ # echo "EOF" >> "$GITHUB_OUTPUT"
78
+ #
79
+ # - name: Review with Ollama
80
+ # env:
81
+ # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82
+ # PR_NUMBER: ${{ github.event.pull_request.number }}
83
+ # OLLAMA_MODEL: "llama3.2" # change to your preferred model
84
+ # run: |
85
+ # PROMPT="Review this Python PR diff for bugs, style issues, and improvements. Be concise.\n\n${{ steps.diff.outputs.content }}"
86
+ # REVIEW=$(curl -s http://localhost:11434/api/generate \
87
+ # -d "{\"model\": \"${OLLAMA_MODEL}\", \"prompt\": \"${PROMPT}\", \"stream\": false}" \
88
+ # | python3 -c "import sys,json; print(json.load(sys.stdin)['response'])")
89
+ # gh pr comment "$PR_NUMBER" --body "## Ollama PR Review (${OLLAMA_MODEL})\n\n${REVIEW}"
90
+
91
+ # Placeholder — remove once an option above is uncommented
92
+ no-op:
93
+ name: LLM review (not configured)
94
+ runs-on: ubuntu-latest
95
+ steps:
96
+ - run: echo "LLM PR review not configured. See .github/workflows/pr-review.yml."