switchly 0.1.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 (214) hide show
  1. switchly-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +58 -0
  2. switchly-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +25 -0
  3. switchly-0.1.0/.github/pull_request_template.md +24 -0
  4. switchly-0.1.0/.github/workflows/ci.yml +136 -0
  5. switchly-0.1.0/.github/workflows/docs.yml +35 -0
  6. switchly-0.1.0/.github/workflows/release.yml +186 -0
  7. switchly-0.1.0/.gitignore +227 -0
  8. switchly-0.1.0/.pre-commit-config.yaml +19 -0
  9. switchly-0.1.0/CONTRIBUTING.md +139 -0
  10. switchly-0.1.0/LICENSE +21 -0
  11. switchly-0.1.0/PKG-INFO +427 -0
  12. switchly-0.1.0/README.md +339 -0
  13. switchly-0.1.0/SECURITY.md +44 -0
  14. switchly-0.1.0/docs/adapters/custom.md +520 -0
  15. switchly-0.1.0/docs/adapters/fastapi.md +847 -0
  16. switchly-0.1.0/docs/adapters/index.md +22 -0
  17. switchly-0.1.0/docs/assets/dashboard.png +0 -0
  18. switchly-0.1.0/docs/assets/logo-full.svg +53 -0
  19. switchly-0.1.0/docs/assets/logo.svg +34 -0
  20. switchly-0.1.0/docs/assets/openapi-maintenance.png +0 -0
  21. switchly-0.1.0/docs/assets/openapi.png +0 -0
  22. switchly-0.1.0/docs/changelog.md +187 -0
  23. switchly-0.1.0/docs/contributing.md +206 -0
  24. switchly-0.1.0/docs/guides/distributed.md +336 -0
  25. switchly-0.1.0/docs/guides/production.md +440 -0
  26. switchly-0.1.0/docs/guides/switchly-server.md +484 -0
  27. switchly-0.1.0/docs/index.md +326 -0
  28. switchly-0.1.0/docs/javascripts/mermaid-init.js +183 -0
  29. switchly-0.1.0/docs/overrides/main.html +70 -0
  30. switchly-0.1.0/docs/reference/backends.md +240 -0
  31. switchly-0.1.0/docs/reference/cli.md +705 -0
  32. switchly-0.1.0/docs/reference/decorators.md +454 -0
  33. switchly-0.1.0/docs/reference/engine.md +744 -0
  34. switchly-0.1.0/docs/reference/exceptions.md +154 -0
  35. switchly-0.1.0/docs/reference/feature-flags.md +481 -0
  36. switchly-0.1.0/docs/reference/middleware.md +173 -0
  37. switchly-0.1.0/docs/reference/models.md +191 -0
  38. switchly-0.1.0/docs/reference/rate-limiting.md +882 -0
  39. switchly-0.1.0/docs/stylesheets/extra.css +675 -0
  40. switchly-0.1.0/docs/tutorial/admin-dashboard.md +187 -0
  41. switchly-0.1.0/docs/tutorial/backends.md +320 -0
  42. switchly-0.1.0/docs/tutorial/cli.md +263 -0
  43. switchly-0.1.0/docs/tutorial/feature-flags.md +467 -0
  44. switchly-0.1.0/docs/tutorial/first-decorator.md +124 -0
  45. switchly-0.1.0/docs/tutorial/installation.md +93 -0
  46. switchly-0.1.0/docs/tutorial/middleware.md +166 -0
  47. switchly-0.1.0/docs/tutorial/rate-limiting.md +470 -0
  48. switchly-0.1.0/examples/__init__.py +0 -0
  49. switchly-0.1.0/examples/fastapi/__init__.py +0 -0
  50. switchly-0.1.0/examples/fastapi/basic.py +137 -0
  51. switchly-0.1.0/examples/fastapi/custom_backend/__init__.py +0 -0
  52. switchly-0.1.0/examples/fastapi/custom_backend/sqlite_backend.py +323 -0
  53. switchly-0.1.0/examples/fastapi/custom_responses.py +210 -0
  54. switchly-0.1.0/examples/fastapi/dependency_injection.py +177 -0
  55. switchly-0.1.0/examples/fastapi/feature_flags.py +439 -0
  56. switchly-0.1.0/examples/fastapi/global_maintenance.py +110 -0
  57. switchly-0.1.0/examples/fastapi/multi_service.py +321 -0
  58. switchly-0.1.0/examples/fastapi/rate_limiting.py +300 -0
  59. switchly-0.1.0/examples/fastapi/scheduled_maintenance.py +97 -0
  60. switchly-0.1.0/examples/fastapi/switchly_server.py +257 -0
  61. switchly-0.1.0/examples/fastapi/webhooks.py +255 -0
  62. switchly-0.1.0/input.css +18 -0
  63. switchly-0.1.0/mkdocs.yml +141 -0
  64. switchly-0.1.0/package.json +9 -0
  65. switchly-0.1.0/pyproject.toml +135 -0
  66. switchly-0.1.0/switchly/__init__.py +169 -0
  67. switchly-0.1.0/switchly/admin/__init__.py +11 -0
  68. switchly-0.1.0/switchly/admin/api.py +1190 -0
  69. switchly-0.1.0/switchly/admin/app.py +598 -0
  70. switchly-0.1.0/switchly/admin/auth.py +259 -0
  71. switchly-0.1.0/switchly/cli/__init__.py +0 -0
  72. switchly-0.1.0/switchly/cli/client.py +567 -0
  73. switchly-0.1.0/switchly/cli/config.py +253 -0
  74. switchly-0.1.0/switchly/cli/main.py +2608 -0
  75. switchly-0.1.0/switchly/core/__init__.py +0 -0
  76. switchly-0.1.0/switchly/core/backends/__init__.py +0 -0
  77. switchly-0.1.0/switchly/core/backends/base.py +413 -0
  78. switchly-0.1.0/switchly/core/backends/file.py +425 -0
  79. switchly-0.1.0/switchly/core/backends/memory.py +183 -0
  80. switchly-0.1.0/switchly/core/backends/redis.py +472 -0
  81. switchly-0.1.0/switchly/core/backends/server.py +794 -0
  82. switchly-0.1.0/switchly/core/config.py +281 -0
  83. switchly-0.1.0/switchly/core/engine.py +2497 -0
  84. switchly-0.1.0/switchly/core/exceptions.py +145 -0
  85. switchly-0.1.0/switchly/core/feature_flags/__init__.py +153 -0
  86. switchly-0.1.0/switchly/core/feature_flags/_context.py +65 -0
  87. switchly-0.1.0/switchly/core/feature_flags/_guard.py +26 -0
  88. switchly-0.1.0/switchly/core/feature_flags/client.py +171 -0
  89. switchly-0.1.0/switchly/core/feature_flags/evaluator.py +443 -0
  90. switchly-0.1.0/switchly/core/feature_flags/hooks.py +168 -0
  91. switchly-0.1.0/switchly/core/feature_flags/models.py +590 -0
  92. switchly-0.1.0/switchly/core/feature_flags/provider.py +199 -0
  93. switchly-0.1.0/switchly/core/feature_flags/scheduler.py +233 -0
  94. switchly-0.1.0/switchly/core/models.py +100 -0
  95. switchly-0.1.0/switchly/core/rate_limit/__init__.py +11 -0
  96. switchly-0.1.0/switchly/core/rate_limit/keys.py +229 -0
  97. switchly-0.1.0/switchly/core/rate_limit/limiter.py +250 -0
  98. switchly-0.1.0/switchly/core/rate_limit/models.py +324 -0
  99. switchly-0.1.0/switchly/core/rate_limit/storage.py +689 -0
  100. switchly-0.1.0/switchly/core/scheduler.py +225 -0
  101. switchly-0.1.0/switchly/core/webhooks.py +60 -0
  102. switchly-0.1.0/switchly/dashboard/__init__.py +5 -0
  103. switchly-0.1.0/switchly/dashboard/app.py +151 -0
  104. switchly-0.1.0/switchly/dashboard/auth.py +61 -0
  105. switchly-0.1.0/switchly/dashboard/routes.py +2161 -0
  106. switchly-0.1.0/switchly/dashboard/static/switchly.min.css +2 -0
  107. switchly-0.1.0/switchly/dashboard/templates/audit.html +90 -0
  108. switchly-0.1.0/switchly/dashboard/templates/base.html +689 -0
  109. switchly-0.1.0/switchly/dashboard/templates/flag_detail.html +773 -0
  110. switchly-0.1.0/switchly/dashboard/templates/flags.html +136 -0
  111. switchly-0.1.0/switchly/dashboard/templates/index.html +142 -0
  112. switchly-0.1.0/switchly/dashboard/templates/login.html +107 -0
  113. switchly-0.1.0/switchly/dashboard/templates/partials/audit_row.html +107 -0
  114. switchly-0.1.0/switchly/dashboard/templates/partials/audit_rows.html +3 -0
  115. switchly-0.1.0/switchly/dashboard/templates/partials/flag_eval_result.html +137 -0
  116. switchly-0.1.0/switchly/dashboard/templates/partials/flag_row.html +131 -0
  117. switchly-0.1.0/switchly/dashboard/templates/partials/flag_rows.html +34 -0
  118. switchly-0.1.0/switchly/dashboard/templates/partials/global_maintenance.html +79 -0
  119. switchly-0.1.0/switchly/dashboard/templates/partials/global_rl_card.html +133 -0
  120. switchly-0.1.0/switchly/dashboard/templates/partials/modal.html +115 -0
  121. switchly-0.1.0/switchly/dashboard/templates/partials/modal_env_gate.html +71 -0
  122. switchly-0.1.0/switchly/dashboard/templates/partials/modal_flag_create.html +105 -0
  123. switchly-0.1.0/switchly/dashboard/templates/partials/modal_flag_eval.html +112 -0
  124. switchly-0.1.0/switchly/dashboard/templates/partials/modal_global_disable.html +51 -0
  125. switchly-0.1.0/switchly/dashboard/templates/partials/modal_global_enable.html +114 -0
  126. switchly-0.1.0/switchly/dashboard/templates/partials/modal_global_rl.html +129 -0
  127. switchly-0.1.0/switchly/dashboard/templates/partials/modal_global_rl_delete.html +39 -0
  128. switchly-0.1.0/switchly/dashboard/templates/partials/modal_global_rl_reset.html +39 -0
  129. switchly-0.1.0/switchly/dashboard/templates/partials/modal_rl_add.html +120 -0
  130. switchly-0.1.0/switchly/dashboard/templates/partials/modal_rl_delete.html +56 -0
  131. switchly-0.1.0/switchly/dashboard/templates/partials/modal_rl_edit.html +109 -0
  132. switchly-0.1.0/switchly/dashboard/templates/partials/modal_rl_reset.html +45 -0
  133. switchly-0.1.0/switchly/dashboard/templates/partials/modal_segment_create.html +86 -0
  134. switchly-0.1.0/switchly/dashboard/templates/partials/modal_segment_detail.html +118 -0
  135. switchly-0.1.0/switchly/dashboard/templates/partials/modal_segment_view.html +151 -0
  136. switchly-0.1.0/switchly/dashboard/templates/partials/modal_service_disable.html +51 -0
  137. switchly-0.1.0/switchly/dashboard/templates/partials/modal_service_enable.html +95 -0
  138. switchly-0.1.0/switchly/dashboard/templates/partials/modal_service_rl.html +131 -0
  139. switchly-0.1.0/switchly/dashboard/templates/partials/modal_service_rl_delete.html +41 -0
  140. switchly-0.1.0/switchly/dashboard/templates/partials/modal_service_rl_reset.html +41 -0
  141. switchly-0.1.0/switchly/dashboard/templates/partials/pagination.html +63 -0
  142. switchly-0.1.0/switchly/dashboard/templates/partials/rate_limit_hits.html +16 -0
  143. switchly-0.1.0/switchly/dashboard/templates/partials/rate_limit_rows.html +61 -0
  144. switchly-0.1.0/switchly/dashboard/templates/partials/route_row.html +179 -0
  145. switchly-0.1.0/switchly/dashboard/templates/partials/routes_table.html +4 -0
  146. switchly-0.1.0/switchly/dashboard/templates/partials/segment_row.html +96 -0
  147. switchly-0.1.0/switchly/dashboard/templates/partials/segment_rows.html +34 -0
  148. switchly-0.1.0/switchly/dashboard/templates/partials/segment_rules_section.html +172 -0
  149. switchly-0.1.0/switchly/dashboard/templates/partials/service_maintenance.html +75 -0
  150. switchly-0.1.0/switchly/dashboard/templates/partials/service_rl_card.html +135 -0
  151. switchly-0.1.0/switchly/dashboard/templates/rate_limits.html +166 -0
  152. switchly-0.1.0/switchly/dashboard/templates/rl_hits.html +50 -0
  153. switchly-0.1.0/switchly/dashboard/templates/segments.html +112 -0
  154. switchly-0.1.0/switchly/fastapi/__init__.py +51 -0
  155. switchly-0.1.0/switchly/fastapi/decorators.py +762 -0
  156. switchly-0.1.0/switchly/fastapi/dependencies.py +123 -0
  157. switchly-0.1.0/switchly/fastapi/middleware.py +506 -0
  158. switchly-0.1.0/switchly/fastapi/openapi.py +633 -0
  159. switchly-0.1.0/switchly/fastapi/router.py +250 -0
  160. switchly-0.1.0/switchly/sdk/__init__.py +254 -0
  161. switchly-0.1.0/switchly/sdk/flag_provider.py +176 -0
  162. switchly-0.1.0/switchly/server/__init__.py +115 -0
  163. switchly-0.1.0/switchly-logo.svg +53 -0
  164. switchly-0.1.0/tests/__init__.py +0 -0
  165. switchly-0.1.0/tests/admin/__init__.py +0 -0
  166. switchly-0.1.0/tests/admin/test_api.py +500 -0
  167. switchly-0.1.0/tests/admin/test_auth.py +306 -0
  168. switchly-0.1.0/tests/admin/test_flag_api.py +651 -0
  169. switchly-0.1.0/tests/core/__init__.py +0 -0
  170. switchly-0.1.0/tests/core/feature_flags/__init__.py +0 -0
  171. switchly-0.1.0/tests/core/feature_flags/test_client.py +270 -0
  172. switchly-0.1.0/tests/core/feature_flags/test_evaluator.py +524 -0
  173. switchly-0.1.0/tests/core/feature_flags/test_models.py +424 -0
  174. switchly-0.1.0/tests/core/feature_flags/test_provider.py +529 -0
  175. switchly-0.1.0/tests/core/feature_flags/test_scheduler.py +348 -0
  176. switchly-0.1.0/tests/core/feature_flags/test_sync_client.py +222 -0
  177. switchly-0.1.0/tests/core/rate_limit/__init__.py +0 -0
  178. switchly-0.1.0/tests/core/rate_limit/test_models.py +177 -0
  179. switchly-0.1.0/tests/core/rate_limit/test_policy_persistence.py +235 -0
  180. switchly-0.1.0/tests/core/rate_limit/test_storage.py +262 -0
  181. switchly-0.1.0/tests/core/test_backends.py +248 -0
  182. switchly-0.1.0/tests/core/test_config.py +113 -0
  183. switchly-0.1.0/tests/core/test_engine.py +673 -0
  184. switchly-0.1.0/tests/core/test_exceptions.py +51 -0
  185. switchly-0.1.0/tests/core/test_models.py +117 -0
  186. switchly-0.1.0/tests/core/test_scheduler.py +198 -0
  187. switchly-0.1.0/tests/core/test_scheduler_polling.py +293 -0
  188. switchly-0.1.0/tests/core/test_webhooks.py +165 -0
  189. switchly-0.1.0/tests/dashboard/__init__.py +0 -0
  190. switchly-0.1.0/tests/dashboard/test_flag_dashboard.py +912 -0
  191. switchly-0.1.0/tests/dashboard/test_rl_dashboard.py +237 -0
  192. switchly-0.1.0/tests/dashboard/test_routes.py +456 -0
  193. switchly-0.1.0/tests/fastapi/__init__.py +0 -0
  194. switchly-0.1.0/tests/fastapi/_helpers.py +21 -0
  195. switchly-0.1.0/tests/fastapi/test_acceptance.py +122 -0
  196. switchly-0.1.0/tests/fastapi/test_decorators.py +188 -0
  197. switchly-0.1.0/tests/fastapi/test_dependencies.py +605 -0
  198. switchly-0.1.0/tests/fastapi/test_deprecated.py +229 -0
  199. switchly-0.1.0/tests/fastapi/test_global_maintenance.py +197 -0
  200. switchly-0.1.0/tests/fastapi/test_global_maintenance_docs.py +211 -0
  201. switchly-0.1.0/tests/fastapi/test_middleware.py +235 -0
  202. switchly-0.1.0/tests/fastapi/test_openapi.py +208 -0
  203. switchly-0.1.0/tests/fastapi/test_openapi_maintenance.py +224 -0
  204. switchly-0.1.0/tests/fastapi/test_parameterized_routes.py +233 -0
  205. switchly-0.1.0/tests/fastapi/test_plain_router.py +209 -0
  206. switchly-0.1.0/tests/fastapi/test_rate_limit.py +286 -0
  207. switchly-0.1.0/tests/fastapi/test_router.py +109 -0
  208. switchly-0.1.0/tests/fastapi/test_startup_scan.py +311 -0
  209. switchly-0.1.0/tests/sdk/__init__.py +0 -0
  210. switchly-0.1.0/tests/sdk/test_flag_provider.py +346 -0
  211. switchly-0.1.0/tests/test_cli.py +677 -0
  212. switchly-0.1.0/tests/test_cli_service.py +241 -0
  213. switchly-0.1.0/tests/test_flags_cli.py +975 -0
  214. switchly-0.1.0/uv.lock +1622 -0
@@ -0,0 +1,58 @@
1
+ name: Bug Report
2
+ description: File a bug report
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for taking the time to fill out this bug report!
9
+
10
+ - type: textarea
11
+ id: description
12
+ attributes:
13
+ label: Description
14
+ description: A clear and concise description of the bug.
15
+ validations:
16
+ required: true
17
+
18
+ - type: textarea
19
+ id: reproduction
20
+ attributes:
21
+ label: Steps to reproduce
22
+ description: Minimal code snippet or steps to reproduce the issue.
23
+ placeholder: |
24
+ ```python
25
+ from switchly.fastapi import ...
26
+ ```
27
+ validations:
28
+ required: true
29
+
30
+ - type: textarea
31
+ id: expected
32
+ attributes:
33
+ label: Expected behaviour
34
+ validations:
35
+ required: true
36
+
37
+ - type: textarea
38
+ id: actual
39
+ attributes:
40
+ label: Actual behaviour
41
+ validations:
42
+ required: true
43
+
44
+ - type: input
45
+ id: version
46
+ attributes:
47
+ label: switchly version
48
+ placeholder: "e.g. 0.1.0"
49
+ validations:
50
+ required: true
51
+
52
+ - type: input
53
+ id: python
54
+ attributes:
55
+ label: Python version
56
+ placeholder: "e.g. 3.13.0"
57
+ validations:
58
+ required: true
@@ -0,0 +1,25 @@
1
+ name: Feature Request
2
+ description: Suggest a new feature or improvement
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem / motivation
9
+ description: What problem does this feature solve?
10
+ validations:
11
+ required: true
12
+
13
+ - type: textarea
14
+ id: solution
15
+ attributes:
16
+ label: Proposed solution
17
+ description: Describe the feature you'd like to see.
18
+ validations:
19
+ required: true
20
+
21
+ - type: textarea
22
+ id: alternatives
23
+ attributes:
24
+ label: Alternatives considered
25
+ description: Any alternative approaches you've considered?
@@ -0,0 +1,24 @@
1
+ ## Description
2
+
3
+ <!-- Describe what this PR does and why. -->
4
+
5
+ ## Type of change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Refactor / cleanup
10
+ - [ ] Documentation
11
+ - [ ] CI / tooling
12
+
13
+ ## Checklist
14
+
15
+ - [ ] Tests added / updated for all changed behaviour
16
+ - [ ] `ruff check .` passes with no errors
17
+ - [ ] `ruff format .` applied
18
+ - [ ] `pytest` passes locally
19
+ - [ ] `switchly.core` has no new imports from `switchly.fastapi`, `switchly.dashboard`, or `switchly.cli`
20
+ - [ ] All new public functions and classes have docstrings
21
+
22
+ ## Related issues
23
+
24
+ <!-- Closes #... -->
@@ -0,0 +1,136 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [develop]
6
+ pull_request:
7
+ branches: [main, develop]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ css:
15
+ name: CSS build check
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Node
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: "20"
24
+
25
+ - name: Install Tailwind
26
+ run: npm install
27
+
28
+ - name: Rebuild CSS
29
+ run: npm run build:css
30
+
31
+ - name: Verify CSS is up-to-date
32
+ run: |
33
+ if ! git diff --quiet switchly/dashboard/static/switchly.min.css; then
34
+ echo "::error::switchly.min.css is out of date. Run 'npm run build:css' locally and commit the result."
35
+ git diff --stat switchly/dashboard/static/switchly.min.css
36
+ exit 1
37
+ fi
38
+
39
+ lint:
40
+ name: Lint & Format
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+
45
+ - name: Set up Python
46
+ uses: actions/setup-python@v5
47
+ with:
48
+ python-version: "3.13"
49
+
50
+ - name: Install uv
51
+ uses: astral-sh/setup-uv@v4
52
+ with:
53
+ enable-cache: true
54
+
55
+ - name: Install dependencies
56
+ run: uv pip install --system ruff mypy
57
+
58
+ - name: Run ruff (lint)
59
+ run: ruff check .
60
+
61
+ - name: Run ruff (format check)
62
+ run: ruff format --check .
63
+
64
+ - name: Run mypy
65
+ run: uv pip install --system -e ".[all,dev]" && mypy switchly
66
+
67
+ test:
68
+ name: Tests (Python ${{ matrix.python-version }}, ${{ matrix.os }})
69
+ runs-on: ${{ matrix.os }}
70
+ strategy:
71
+ fail-fast: false
72
+ matrix:
73
+ python-version: ["3.11", "3.12", "3.13"]
74
+ os: [ubuntu-latest, macos-latest, windows-latest]
75
+
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+
79
+ - name: Set up Python ${{ matrix.python-version }}
80
+ uses: actions/setup-python@v5
81
+ with:
82
+ python-version: ${{ matrix.python-version }}
83
+
84
+ - name: Install uv
85
+ uses: astral-sh/setup-uv@v4
86
+ with:
87
+ enable-cache: true
88
+
89
+ - name: Install dependencies
90
+ run: uv pip install --system -e ".[all,dev]"
91
+
92
+ - name: Run tests
93
+ run: pytest --tb=short -q
94
+
95
+ test-redis:
96
+ name: Tests with Redis
97
+ runs-on: ubuntu-latest
98
+
99
+ steps:
100
+ - uses: actions/checkout@v4
101
+
102
+ - name: Set cache week key
103
+ id: date
104
+ run: echo "week=$(date +'%Y-%U')" >> $GITHUB_OUTPUT
105
+
106
+ - name: Cache apt packages
107
+ uses: actions/cache@v4
108
+ with:
109
+ path: /var/cache/apt/archives
110
+ key: apt-redis-${{ runner.os }}-${{ steps.date.outputs.week }}
111
+ restore-keys: apt-redis-${{ runner.os }}-
112
+
113
+ - name: Start Redis
114
+ run: |
115
+ sudo apt-get update
116
+ sudo apt-get install -y redis-server
117
+ redis-server --daemonize yes --port 6379
118
+ redis-cli ping
119
+
120
+ - name: Set up Python
121
+ uses: actions/setup-python@v5
122
+ with:
123
+ python-version: "3.13"
124
+
125
+ - name: Install uv
126
+ uses: astral-sh/setup-uv@v4
127
+ with:
128
+ enable-cache: true
129
+
130
+ - name: Install dependencies
131
+ run: uv pip install --system -e ".[all,dev]"
132
+
133
+ - name: Run tests with Redis
134
+ run: pytest --tb=short -q
135
+ env:
136
+ SWITCHLY_REDIS_URL: redis://localhost:6379
@@ -0,0 +1,35 @@
1
+ name: Publish Docs
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "docs/**"
9
+ - "mkdocs.yml"
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0 # full history required by mkdocs gh-deploy
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.13"
27
+
28
+ - name: Install uv
29
+ run: pip install uv
30
+
31
+ - name: Install docs dependencies
32
+ run: uv pip install --system ".[docs]"
33
+
34
+ - name: Deploy to GitHub Pages
35
+ run: mkdocs gh-deploy --force
@@ -0,0 +1,186 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ concurrency:
8
+ group: release
9
+ cancel-in-progress: false
10
+
11
+ jobs:
12
+ # ── 1. Check whether this push actually bumped the version ──────────────────
13
+ check-version:
14
+ name: Check version bump
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ version: ${{ steps.get-version.outputs.version }}
18
+ tag: ${{ steps.get-version.outputs.tag }}
19
+ should-release: ${{ steps.compare.outputs.should-release }}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0 # need full history to list tags
24
+
25
+ - name: Extract version from pyproject.toml
26
+ id: get-version
27
+ run: |
28
+ VERSION=$(python - <<'EOF'
29
+ import tomllib
30
+ with open("pyproject.toml", "rb") as f:
31
+ print(tomllib.load(f)["project"]["version"])
32
+ EOF
33
+ )
34
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
35
+ echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
36
+
37
+ - name: Check if tag already exists
38
+ id: compare
39
+ run: |
40
+ TAG="v${{ steps.get-version.outputs.version }}"
41
+ if git rev-parse "$TAG" >/dev/null 2>&1; then
42
+ echo "Tag $TAG already exists — skipping release."
43
+ echo "should-release=false" >> "$GITHUB_OUTPUT"
44
+ else
45
+ echo "New version $TAG detected — will release."
46
+ echo "should-release=true" >> "$GITHUB_OUTPUT"
47
+ fi
48
+
49
+ # ── 2. Full CI gate (reuse existing checks) ────────────────────────────────
50
+ ci-gate:
51
+ name: CI gate
52
+ needs: check-version
53
+ if: needs.check-version.outputs.should-release == 'true'
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+
58
+ - uses: actions/setup-node@v4
59
+ with:
60
+ node-version: "20"
61
+
62
+ - name: Rebuild CSS & verify
63
+ run: |
64
+ npm install
65
+ npm run build:css
66
+ if ! git diff --quiet switchly/dashboard/static/switchly.min.css; then
67
+ echo "::error::switchly.min.css is out of date."
68
+ exit 1
69
+ fi
70
+
71
+ - uses: actions/setup-python@v5
72
+ with:
73
+ python-version: "3.13"
74
+
75
+ - uses: astral-sh/setup-uv@v4
76
+
77
+ - name: Lint
78
+ run: |
79
+ uv pip install --system ruff
80
+ ruff check .
81
+ ruff format --check .
82
+
83
+ - name: Tests
84
+ run: |
85
+ uv pip install --system -e ".[all,dev]"
86
+ pytest --tb=short -q
87
+
88
+ # ── 3. Build distribution ──────────────────────────────────────────────────
89
+ build:
90
+ name: Build distribution
91
+ needs: [check-version, ci-gate]
92
+ if: needs.check-version.outputs.should-release == 'true'
93
+ runs-on: ubuntu-latest
94
+ steps:
95
+ - uses: actions/checkout@v4
96
+
97
+ - uses: actions/setup-python@v5
98
+ with:
99
+ python-version: "3.13"
100
+
101
+ - uses: astral-sh/setup-uv@v4
102
+
103
+ - name: Build wheel & sdist
104
+ run: |
105
+ uv pip install --system build
106
+ python -m build
107
+
108
+ - name: Upload dist artifact
109
+ uses: actions/upload-artifact@v4
110
+ with:
111
+ name: dist
112
+ path: dist/
113
+ retention-days: 1
114
+
115
+ # ── 4. Publish to PyPI (OIDC trusted publishing) ──────────────────────────
116
+ publish-pypi:
117
+ name: Publish to PyPI
118
+ needs: [check-version, build]
119
+ if: needs.check-version.outputs.should-release == 'true'
120
+ runs-on: ubuntu-latest
121
+ environment: pypi # protects the OIDC token — require approval if desired
122
+ permissions:
123
+ id-token: write # required for OIDC trusted publishing
124
+ steps:
125
+ - name: Download dist artifact
126
+ uses: actions/download-artifact@v4
127
+ with:
128
+ name: dist
129
+ path: dist/
130
+
131
+ - name: Publish to PyPI
132
+ uses: pypa/gh-action-pypi-publish@release/v1
133
+
134
+ # ── 5. Create GitHub tag & release ────────────────────────────────────────
135
+ github-release:
136
+ name: Create GitHub Release
137
+ needs: [check-version, publish-pypi]
138
+ if: needs.check-version.outputs.should-release == 'true'
139
+ runs-on: ubuntu-latest
140
+ permissions:
141
+ contents: write # required to create tags and releases
142
+ steps:
143
+ - uses: actions/checkout@v4
144
+ with:
145
+ fetch-depth: 0
146
+
147
+ - name: Download dist artifact
148
+ uses: actions/download-artifact@v4
149
+ with:
150
+ name: dist
151
+ path: dist/
152
+
153
+ - name: Extract changelog for this version
154
+ id: changelog
155
+ run: |
156
+ VERSION="${{ needs.check-version.outputs.version }}"
157
+ # Extract the section for this version from CHANGELOG.md (if it exists)
158
+ if [ -f CHANGELOG.md ]; then
159
+ NOTES=$(python - <<EOF
160
+ import re, sys
161
+ text = open("CHANGELOG.md").read()
162
+ # Match ## [x.y.z] or ## x.y.z heading up to the next ## heading
163
+ pattern = rf"(?:^|\n)## \[?{re.escape("$VERSION")}\]?[^\n]*\n(.*?)(?=\n## |\Z)"
164
+ m = re.search(pattern, text, re.DOTALL)
165
+ print(m.group(1).strip() if m else "")
166
+ EOF
167
+ )
168
+ else
169
+ NOTES=""
170
+ fi
171
+ # Fallback to a plain message when changelog is absent/empty
172
+ if [ -z "$NOTES" ]; then
173
+ NOTES="Release $VERSION — see commit history for details."
174
+ fi
175
+ # Write to a file to avoid quoting issues
176
+ echo "$NOTES" > release_notes.txt
177
+
178
+ - name: Create tag and GitHub Release
179
+ env:
180
+ GH_TOKEN: ${{ github.token }}
181
+ run: |
182
+ TAG="${{ needs.check-version.outputs.tag }}"
183
+ VERSION="${{ needs.check-version.outputs.version }}"
184
+ gh release create "$TAG" dist/* \
185
+ --title "switchly $VERSION" \
186
+ --notes-file release_notes.txt
@@ -0,0 +1,227 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
217
+
218
+
219
+ # Project specific ignores
220
+ .shield
221
+ CLAUDE.md
222
+
223
+ # Node / Tailwind build tooling (dev only — not part of the Python package)
224
+ node_modules/
225
+ package-lock.json
226
+ # NOTE: shield/dashboard/static/shield.min.css IS committed.
227
+ # pip users have no npm, so the pre-built CSS must ship with the package.
@@ -0,0 +1,19 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-toml
9
+ - id: check-merge-conflict
10
+ - id: check-added-large-files
11
+ args: ["--maxkb=500"]
12
+ - id: debug-statements
13
+
14
+ - repo: https://github.com/astral-sh/ruff-pre-commit
15
+ rev: v0.9.10
16
+ hooks:
17
+ - id: ruff
18
+ args: [--fix]
19
+ - id: ruff-format