clash-sub-manager 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.
- clash_sub_manager-0.1.0/PKG-INFO +17 -0
- clash_sub_manager-0.1.0/README.md +0 -0
- clash_sub_manager-0.1.0/pyproject.toml +154 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/__init__.py +5 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/__main__.py +4 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/__init__.py +3 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/dependencies.py +19 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/__init__.py +22 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/_db.py +34 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/_rule_providers.py +26 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/composite_templates.py +204 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/config.py +172 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/convert.py +29 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/merge.py +29 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/merge_profiles.py +254 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/rules.py +93 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/routes/template_patches.py +118 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/schemas.py +289 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/schemas_patch.py +184 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/api/server.py +46 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/cli.py +38 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/__init__.py +20 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/composer.py +48 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/converter.py +125 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/fetcher.py +44 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/merger.py +102 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/patch.py +317 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/rules.py +91 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/core/template.py +213 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/db/__init__.py +20 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/db/base.py +21 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/db/models.py +88 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/db/models_patch.py +47 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/db/session.py +52 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/models/__init__.py +21 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/models/clash.py +26 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/models/proxy.py +88 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/models/subscription.py +35 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/__init__.py +84 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/base.py +101 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/clash.py +157 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/ss.py +73 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/ssr.py +49 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/trojan.py +56 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/parsers/vmess.py +65 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/.gitkeep +0 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/abap-DLDM7-KI.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/apex-DNDY2TF8.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/azcli-Y6nb8tq_.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/bat-BwHxbl9M.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/bicep-CFznDFnq.js +2 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/cameligo-Bf6VGUru.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/clojure-Dnu-v4kV.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/codicon-ngg6Pgfi.ttf +0 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/coffee-Bd8akH9Z.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/cpp-BbWJElDN.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/csharp-Co3qMtFm.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/csp-D-4FJmMZ.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/css-DdJfP1eB.js +3 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/css.worker-DBVD8oXr.js +93 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/cssMode-CEwRweiE.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/cypher-cTPe9QuQ.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/dart-BOtBlQCF.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/dockerfile-BG73LgW2.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/ecl-BEgZUVRK.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/editor.worker-CdQrwHl8.js +26 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/elixir-BkW5O-1t.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/flow9-BeJ5waoc.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/freemarker2-C8vB75nR.js +3 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/fsharp-PahG7c26.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/go-acbASCJo.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/graphql-BxJiqAUM.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/handlebars-CoS2kdk8.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/hcl-DtV1sZF8.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/html-DYVgKgkF.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/html.worker-CwpTb9lJ.js +470 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/htmlMode-_KQ2OpU9.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/index-BcrtEVFj.js +943 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/index-DIAjxuWt.css +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/ini-Kd9XrMLS.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/java-CXBNlu9o.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/javascript-kytjXFg_.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/json.worker-BoL8UZqY.js +58 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/jsonMode-CK5jLb5D.js +7 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/julia-cl7-CwDS.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/kotlin-s7OhZKlX.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/less-9HpZscsL.js +2 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/lexon-OrD6JF1K.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/liquid-7rnuAQ6t.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/lspLanguageFeatures-CCNtv2Fl.js +4 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/lua-Cyyb5UIc.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/m3-B8OfTtLu.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/markdown-BFxVWTOG.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/mdx-C64lqVrO.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/mips-CiqrrVzr.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/msdax-DmeGPVcC.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/mysql-C_tMU-Nz.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/objective-c-BDtDVThU.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/pascal-vHIfCaH5.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/pascaligo-DtZ0uQbO.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/perl-Ub6l9XKa.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/pgsql-BlNEE0v7.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/php-BBUBE1dy.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/pla-DSh2-awV.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/postiats-CocnycG-.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/powerquery-tScXyioY.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/powershell-COWaemsV.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/protobuf-Brw8urJB.js +2 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/pug-8SOpv6rk.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/python-CkjFJQTJ.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/qsharp-Bw9ernYp.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/r-j7ic8hl3.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/razor-DlJimliK.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/redis-Bu5POkcn.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/redshift-Bs9aos_-.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/restructuredtext-CqXO7rUv.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/ruby-zBfavPgS.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/rust-BzKRNQWT.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/sb-BBc9UKZt.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/scala-D9hQfWCl.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/scheme-BPhDTwHR.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/scss-CBJaRo0y.js +3 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/shell-DiJ1NA_G.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/solidity-Db0IVjzk.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/sophia-CnS9iZB_.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/sparql-CJmd_6j2.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/sql-ClhHkBeG.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/st-CHwy0fLd.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/swift-CnmFD0ga.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/systemverilog-Bs9z6M-B.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/tcl-Dm6ycUr_.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/ts.worker-BH9nVgjN.js +67718 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/tsMode-Ct41V8vy.js +11 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/twig-Csy3S7wG.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/typescript-CMZ7uylT.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/typespec-Btyra-wh.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/vb-Db0cS2oM.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/wgsl-BTesnYfV.js +298 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/xml-Dj5Mm3BH.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/assets/yaml-CrX2yQz_.js +1 -0
- clash_sub_manager-0.1.0/src/clash_sub_manager/static/webui/index.html +13 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: clash-sub-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Clash subscription management web service
|
|
5
|
+
Author: shoucandanghehe
|
|
6
|
+
Author-email: shoucandanghehe <wallfjjd@gmail.com>
|
|
7
|
+
Requires-Dist: fastapi>=0.135.1
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Dist: uvicorn>=0.38.0
|
|
10
|
+
Requires-Dist: cyclopts>=4.9.0
|
|
11
|
+
Requires-Dist: httpx>=0.27.0
|
|
12
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
13
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
14
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clash-sub-manager"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Clash subscription management web service"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "shoucandanghehe", email = "wallfjjd@gmail.com" }]
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"fastapi>=0.135.1",
|
|
10
|
+
"pydantic>=2.12.5",
|
|
11
|
+
"uvicorn>=0.38.0",
|
|
12
|
+
"cyclopts>=4.9.0",
|
|
13
|
+
"httpx>=0.27.0",
|
|
14
|
+
"pyyaml>=6.0.1",
|
|
15
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
16
|
+
"aiosqlite>=0.20.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
clash-sub-manager = "clash_sub_manager.cli:main"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.10.6,<0.11.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[tool.fastapi]
|
|
27
|
+
entrypoint = "clash_sub_manager.api.server:app"
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
line-length = 120
|
|
31
|
+
target-version = "py310"
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = [
|
|
35
|
+
"F", # pyflakes
|
|
36
|
+
"E", # pycodestyle errors
|
|
37
|
+
"W", # pycodestyle warnings
|
|
38
|
+
"C90", # mccabe
|
|
39
|
+
"I", # isort
|
|
40
|
+
"N", # PEP8-naming
|
|
41
|
+
"UP", # pyupgrade
|
|
42
|
+
"YTT", # flake8-2020
|
|
43
|
+
"ANN", # flake8-annotations
|
|
44
|
+
"ASYNC", # flake8-async
|
|
45
|
+
"S", # flake8-bandit
|
|
46
|
+
"BLE", # flake8-blind-except
|
|
47
|
+
"FBT", # flake8-boolean-trap
|
|
48
|
+
"B", # flake8-bugbear
|
|
49
|
+
"A", # flake8-builtins
|
|
50
|
+
"COM", # flake8-commas
|
|
51
|
+
"C4", # flake8-comprehensions
|
|
52
|
+
"DTZ", # flake8-datetimez
|
|
53
|
+
"T10", # flake8-debugger
|
|
54
|
+
"EM", # flake8-errmsg
|
|
55
|
+
"FA", # flake8-future-annotations
|
|
56
|
+
"ISC", # flake8-implicit-str-concat
|
|
57
|
+
"ICN", # flake8-import-conventions
|
|
58
|
+
"PIE", # flake8-pie
|
|
59
|
+
"T20", # flake8-print
|
|
60
|
+
"PYI", # flake8-pyi
|
|
61
|
+
"Q", # flake8-quotes
|
|
62
|
+
"RSE", # flake8-raise
|
|
63
|
+
"RET", # flake8-return
|
|
64
|
+
"SLF", # flake8-self
|
|
65
|
+
"SLOT", # flake8-slots
|
|
66
|
+
"SIM", # flake8-simplify
|
|
67
|
+
"TID", # flake8-tidy-imports
|
|
68
|
+
"TC", # flake8-type-checking
|
|
69
|
+
"ARG", # flake8-unused-arguments
|
|
70
|
+
"PTH", # flake8-use-pathlib
|
|
71
|
+
"ERA", # eradicate
|
|
72
|
+
"PD", # pandas-vet
|
|
73
|
+
"PGH", # pygrep-hooks
|
|
74
|
+
"PL", # pylint
|
|
75
|
+
"TRY", # tryceratops
|
|
76
|
+
"FLY", # flynt
|
|
77
|
+
"FAST", # FastAPI
|
|
78
|
+
"PERF", # Perflint
|
|
79
|
+
"FURB", # refurb
|
|
80
|
+
"RUF", # Ruff-specific rules
|
|
81
|
+
]
|
|
82
|
+
ignore = [
|
|
83
|
+
"E501", # 过长的行由 ruff format 处理, 剩余的都是字符串
|
|
84
|
+
"TRY003",
|
|
85
|
+
"COM812", # 强制尾随逗号
|
|
86
|
+
"TID252", # 相对导入
|
|
87
|
+
"ISC001", # format warning
|
|
88
|
+
]
|
|
89
|
+
flake8-quotes = { inline-quotes = "single", multiline-quotes = "double" }
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.flake8-annotations]
|
|
92
|
+
mypy-init-return = true
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.flake8-builtins]
|
|
95
|
+
builtins-ignorelist = ["id"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.per-file-ignores]
|
|
98
|
+
"tests/**/*.py" = ["S101", "S603", "PLR2004"]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.format]
|
|
101
|
+
quote-style = "single"
|
|
102
|
+
|
|
103
|
+
[tool.basedpyright]
|
|
104
|
+
pythonVersion = "3.10"
|
|
105
|
+
pythonPlatform = "All"
|
|
106
|
+
typeCheckingMode = "standard"
|
|
107
|
+
reportExplicitAny = 'hint'
|
|
108
|
+
reportUnnecessaryTypeIgnoreComment = 'error'
|
|
109
|
+
reportImplicitOverride = 'error'
|
|
110
|
+
reportUnnecessaryComparison = 'error'
|
|
111
|
+
reportImplicitAbstractClass = 'error'
|
|
112
|
+
enableTypeIgnoreComments = false
|
|
113
|
+
|
|
114
|
+
[tool.bumpversion]
|
|
115
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
116
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
117
|
+
search = "{current_version}"
|
|
118
|
+
replace = "{new_version}"
|
|
119
|
+
regex = false
|
|
120
|
+
ignore_missing_version = false
|
|
121
|
+
ignore_missing_files = false
|
|
122
|
+
tag = true
|
|
123
|
+
sign_tags = true
|
|
124
|
+
tag_name = "v{new_version}"
|
|
125
|
+
tag_message = "🔖 bump version {new_version}"
|
|
126
|
+
allow_dirty = true
|
|
127
|
+
commit = true
|
|
128
|
+
message = "🔖 bump version {new_version}"
|
|
129
|
+
moveable_tags = []
|
|
130
|
+
commit_args = ""
|
|
131
|
+
setup_hooks = []
|
|
132
|
+
pre_commit_hooks = []
|
|
133
|
+
post_commit_hooks = []
|
|
134
|
+
|
|
135
|
+
[[tool.bumpversion.files]]
|
|
136
|
+
filename = "pyproject.toml"
|
|
137
|
+
search = "version = \"{current_version}\""
|
|
138
|
+
replace = "version = \"{new_version}\""
|
|
139
|
+
|
|
140
|
+
[[tool.bumpversion.files]]
|
|
141
|
+
filename = "src/clash_sub_manager/__init__.py"
|
|
142
|
+
search = "__version__ = '{current_version}'"
|
|
143
|
+
replace = "__version__ = '{new_version}'"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
[dependency-groups]
|
|
147
|
+
dev = [
|
|
148
|
+
"basedpyright>=1.38.2",
|
|
149
|
+
"bump-my-version>=1.2.4",
|
|
150
|
+
"mypy>=1.19.1",
|
|
151
|
+
"ruff>=0.15.6",
|
|
152
|
+
"pytest>=8.4.2",
|
|
153
|
+
"pytest-asyncio>=1.2.0",
|
|
154
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""FastAPI dependency helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_session_factory(request: Request) -> async_sessionmaker[AsyncSession]:
|
|
10
|
+
return request.app.state.session_factory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def get_db_session(request: Request) -> AsyncIterator[AsyncSession]:
|
|
14
|
+
session_factory = get_session_factory(request)
|
|
15
|
+
async with session_factory() as session:
|
|
16
|
+
yield session
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ['get_db_session', 'get_session_factory']
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from .composite_templates import router as composite_templates_router
|
|
6
|
+
from .config import router as config_router
|
|
7
|
+
from .convert import router as convert_router
|
|
8
|
+
from .merge import router as merge_router
|
|
9
|
+
from .merge_profiles import router as merge_profiles_router
|
|
10
|
+
from .rules import router as rules_router
|
|
11
|
+
from .template_patches import router as template_patches_router
|
|
12
|
+
|
|
13
|
+
api_router = APIRouter()
|
|
14
|
+
api_router.include_router(convert_router)
|
|
15
|
+
api_router.include_router(merge_router)
|
|
16
|
+
api_router.include_router(merge_profiles_router)
|
|
17
|
+
api_router.include_router(composite_templates_router)
|
|
18
|
+
api_router.include_router(template_patches_router)
|
|
19
|
+
api_router.include_router(config_router)
|
|
20
|
+
api_router.include_router(rules_router)
|
|
21
|
+
|
|
22
|
+
__all__ = ['api_router']
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared database helpers for API routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from fastapi import HTTPException, status
|
|
8
|
+
from sqlalchemy.exc import IntegrityError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _is_unique_name_violation(exc: IntegrityError, *, table_name: str) -> bool:
|
|
15
|
+
message = str(exc.orig).lower()
|
|
16
|
+
return 'unique' in message and (
|
|
17
|
+
f'{table_name}.name' in message or f'uq_{table_name}_name' in message
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def commit_or_name_conflict(db: AsyncSession, *, resource_name: str, table_name: str) -> None:
|
|
22
|
+
try:
|
|
23
|
+
await db.commit()
|
|
24
|
+
except IntegrityError as exc:
|
|
25
|
+
await db.rollback()
|
|
26
|
+
if _is_unique_name_violation(exc, table_name=table_name):
|
|
27
|
+
raise HTTPException(
|
|
28
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
29
|
+
detail=f'{resource_name} name already exists',
|
|
30
|
+
) from exc
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ['commit_or_name_conflict']
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Helpers for rewriting template rule-providers to project-served cached URLs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import select
|
|
8
|
+
|
|
9
|
+
from ...db.models import RuleSource
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from fastapi import Request
|
|
13
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def build_cached_rule_provider_urls(db: AsyncSession, request: Request) -> dict[str, str]:
|
|
17
|
+
sources = list((await db.scalars(select(RuleSource).order_by(RuleSource.id))).all())
|
|
18
|
+
mappings: dict[str, str] = {}
|
|
19
|
+
for source in sources:
|
|
20
|
+
cached_url = str(request.url_for('get_cached_rule_provider', rule_source_id=source.id))
|
|
21
|
+
mappings[source.name] = cached_url
|
|
22
|
+
mappings[source.url] = cached_url
|
|
23
|
+
return mappings
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ['build_cached_rule_provider_urls']
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""CRUD endpoints for composed templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
from sqlalchemy.orm import selectinload
|
|
11
|
+
|
|
12
|
+
from ...core import PatchValidationError, TemplateComposer
|
|
13
|
+
from ...db import CompositeTemplate, MergeProfile, Template, TemplatePatch
|
|
14
|
+
from ..dependencies import get_db_session
|
|
15
|
+
from ..schemas import TemplateSummaryRead
|
|
16
|
+
from ..schemas_patch import (
|
|
17
|
+
CompositePreviewRead,
|
|
18
|
+
CompositeTemplateCreate,
|
|
19
|
+
CompositeTemplatePreviewRequest,
|
|
20
|
+
CompositeTemplateRead,
|
|
21
|
+
CompositeTemplateUpdate,
|
|
22
|
+
TemplatePatchSummaryRead,
|
|
23
|
+
)
|
|
24
|
+
from ._db import commit_or_name_conflict
|
|
25
|
+
|
|
26
|
+
router = APIRouter(tags=['composite-templates'])
|
|
27
|
+
DbSession = Annotated[AsyncSession, Depends(get_db_session)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _serialize_patch_summary(patch: TemplatePatch) -> TemplatePatchSummaryRead:
|
|
31
|
+
return TemplatePatchSummaryRead.model_validate(patch)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _serialize_composite_template(composite: CompositeTemplate, patches: list[TemplatePatch]) -> CompositeTemplateRead:
|
|
35
|
+
return CompositeTemplateRead(
|
|
36
|
+
id=composite.id,
|
|
37
|
+
name=composite.name,
|
|
38
|
+
base_template_id=composite.base_template_id,
|
|
39
|
+
patch_sequence=list(composite.patch_sequence),
|
|
40
|
+
cached_content=composite.cached_content,
|
|
41
|
+
created_at=composite.created_at,
|
|
42
|
+
updated_at=composite.updated_at,
|
|
43
|
+
base_template=TemplateSummaryRead.model_validate(composite.base_template),
|
|
44
|
+
patches=[_serialize_patch_summary(patch) for patch in patches],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _get_template_or_404(db: AsyncSession, template_id: int) -> Template:
|
|
49
|
+
template = await db.get(Template, template_id)
|
|
50
|
+
if template is None:
|
|
51
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='template not found')
|
|
52
|
+
return template
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _get_patches_or_404(db: AsyncSession, patch_sequence: list[int]) -> list[TemplatePatch]:
|
|
56
|
+
if not patch_sequence:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
unique_patch_ids = sorted(set(patch_sequence))
|
|
60
|
+
patches = list((await db.scalars(select(TemplatePatch).where(TemplatePatch.id.in_(unique_patch_ids)))).all())
|
|
61
|
+
patch_by_id = {patch.id: patch for patch in patches}
|
|
62
|
+
missing_ids = [patch_id for patch_id in patch_sequence if patch_id not in patch_by_id]
|
|
63
|
+
if missing_ids:
|
|
64
|
+
missing = ', '.join(str(patch_id) for patch_id in dict.fromkeys(missing_ids))
|
|
65
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'template patches not found: {missing}')
|
|
66
|
+
return [patch_by_id[patch_id] for patch_id in patch_sequence]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _get_composite_template_or_404(composite_id: int, db: AsyncSession) -> CompositeTemplate:
|
|
70
|
+
statement = (
|
|
71
|
+
select(CompositeTemplate)
|
|
72
|
+
.options(selectinload(CompositeTemplate.base_template))
|
|
73
|
+
.where(CompositeTemplate.id == composite_id)
|
|
74
|
+
)
|
|
75
|
+
composite_template = (await db.scalars(statement)).one_or_none()
|
|
76
|
+
if composite_template is None:
|
|
77
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='composite template not found')
|
|
78
|
+
return composite_template
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _render_cached_content(
|
|
82
|
+
composite_template: CompositeTemplate,
|
|
83
|
+
db: AsyncSession,
|
|
84
|
+
) -> tuple[str, list[TemplatePatch]]:
|
|
85
|
+
patches = await _get_patches_or_404(db, composite_template.patch_sequence)
|
|
86
|
+
try:
|
|
87
|
+
cached_content = TemplateComposer().render_cached_content(composite_template.base_template, patches)
|
|
88
|
+
except (PatchValidationError, TypeError, ValueError) as exc:
|
|
89
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
|
90
|
+
return cached_content, patches
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get('/composite-templates', response_model=list[CompositeTemplateRead])
|
|
94
|
+
async def list_composite_templates(db: DbSession) -> list[CompositeTemplateRead]:
|
|
95
|
+
statement = select(CompositeTemplate).options(selectinload(CompositeTemplate.base_template)).order_by(CompositeTemplate.id)
|
|
96
|
+
composites = list((await db.scalars(statement)).all())
|
|
97
|
+
|
|
98
|
+
changed = False
|
|
99
|
+
serialized: list[CompositeTemplateRead] = []
|
|
100
|
+
for composite in composites:
|
|
101
|
+
cached_content, patches = await _render_cached_content(composite, db)
|
|
102
|
+
if composite.cached_content != cached_content:
|
|
103
|
+
composite.cached_content = cached_content
|
|
104
|
+
changed = True
|
|
105
|
+
serialized.append(_serialize_composite_template(composite, patches))
|
|
106
|
+
|
|
107
|
+
if changed:
|
|
108
|
+
await db.commit()
|
|
109
|
+
return serialized
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.post('/composite-templates', response_model=CompositeTemplateRead, status_code=status.HTTP_201_CREATED)
|
|
113
|
+
async def create_composite_template(payload: CompositeTemplateCreate, db: DbSession) -> CompositeTemplateRead:
|
|
114
|
+
base_template = await _get_template_or_404(db, payload.base_template_id)
|
|
115
|
+
patches = await _get_patches_or_404(db, payload.patch_sequence)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
cached_content = TemplateComposer().render_cached_content(base_template, patches)
|
|
119
|
+
except (PatchValidationError, TypeError, ValueError) as exc:
|
|
120
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
|
121
|
+
|
|
122
|
+
composite = CompositeTemplate(
|
|
123
|
+
name=payload.name,
|
|
124
|
+
base_template_id=payload.base_template_id,
|
|
125
|
+
patch_sequence=list(payload.patch_sequence),
|
|
126
|
+
cached_content=cached_content,
|
|
127
|
+
)
|
|
128
|
+
composite.base_template = base_template
|
|
129
|
+
db.add(composite)
|
|
130
|
+
await commit_or_name_conflict(db, resource_name='composite template', table_name='composite_templates')
|
|
131
|
+
await db.refresh(composite)
|
|
132
|
+
return _serialize_composite_template(composite, patches)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.get('/composite-templates/{composite_id}', response_model=CompositeTemplateRead)
|
|
136
|
+
async def get_composite_template(composite_id: int, db: DbSession) -> CompositeTemplateRead:
|
|
137
|
+
composite = await _get_composite_template_or_404(composite_id, db)
|
|
138
|
+
cached_content, patches = await _render_cached_content(composite, db)
|
|
139
|
+
if composite.cached_content != cached_content:
|
|
140
|
+
composite.cached_content = cached_content
|
|
141
|
+
await db.commit()
|
|
142
|
+
await db.refresh(composite)
|
|
143
|
+
return _serialize_composite_template(composite, patches)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.put('/composite-templates/{composite_id}', response_model=CompositeTemplateRead)
|
|
147
|
+
async def update_composite_template(
|
|
148
|
+
composite_id: int,
|
|
149
|
+
payload: CompositeTemplateUpdate,
|
|
150
|
+
db: DbSession,
|
|
151
|
+
) -> CompositeTemplateRead:
|
|
152
|
+
composite = await _get_composite_template_or_404(composite_id, db)
|
|
153
|
+
|
|
154
|
+
name = payload.name if 'name' in payload.model_fields_set and payload.name is not None else composite.name
|
|
155
|
+
base_template_id = (
|
|
156
|
+
payload.base_template_id
|
|
157
|
+
if 'base_template_id' in payload.model_fields_set and payload.base_template_id is not None
|
|
158
|
+
else composite.base_template_id
|
|
159
|
+
)
|
|
160
|
+
patch_sequence = list(payload.patch_sequence) if payload.patch_sequence is not None else list(composite.patch_sequence)
|
|
161
|
+
|
|
162
|
+
base_template = await _get_template_or_404(db, base_template_id)
|
|
163
|
+
patches = await _get_patches_or_404(db, patch_sequence)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
cached_content = TemplateComposer().render_cached_content(base_template, patches)
|
|
167
|
+
except (PatchValidationError, TypeError, ValueError) as exc:
|
|
168
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
|
169
|
+
|
|
170
|
+
composite.name = name
|
|
171
|
+
composite.base_template_id = base_template_id
|
|
172
|
+
composite.base_template = base_template
|
|
173
|
+
composite.patch_sequence = patch_sequence
|
|
174
|
+
composite.cached_content = cached_content
|
|
175
|
+
|
|
176
|
+
await commit_or_name_conflict(db, resource_name='composite template', table_name='composite_templates')
|
|
177
|
+
await db.refresh(composite)
|
|
178
|
+
return _serialize_composite_template(composite, patches)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.delete('/composite-templates/{composite_id}', status_code=status.HTTP_204_NO_CONTENT)
|
|
182
|
+
async def delete_composite_template(composite_id: int, db: DbSession) -> Response:
|
|
183
|
+
composite = await _get_composite_template_or_404(composite_id, db)
|
|
184
|
+
merge_profile = await db.scalar(select(MergeProfile.name).where(MergeProfile.composite_template_id == composite_id))
|
|
185
|
+
if merge_profile is not None:
|
|
186
|
+
raise HTTPException(
|
|
187
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
188
|
+
detail=f'composite template is used by merge profile: {merge_profile}',
|
|
189
|
+
)
|
|
190
|
+
await db.delete(composite)
|
|
191
|
+
await db.commit()
|
|
192
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.post('/composite-templates/preview', response_model=CompositePreviewRead)
|
|
196
|
+
async def preview_composite_template(payload: CompositeTemplatePreviewRequest, db: DbSession) -> CompositePreviewRead:
|
|
197
|
+
base_template = await _get_template_or_404(db, payload.base_template_id)
|
|
198
|
+
patches = await _get_patches_or_404(db, payload.patch_sequence)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
content = TemplateComposer().render_cached_content(base_template, patches)
|
|
202
|
+
return CompositePreviewRead(content=content)
|
|
203
|
+
except (PatchValidationError, TypeError, ValueError) as exc:
|
|
204
|
+
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""CRUD endpoints for subscriptions and templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from ...db.models import CompositeTemplate, Subscription, Template
|
|
12
|
+
from ...models import SubscriptionConfig
|
|
13
|
+
from ..dependencies import get_db_session
|
|
14
|
+
from ..schemas import (
|
|
15
|
+
SubscriptionCreate,
|
|
16
|
+
SubscriptionRead,
|
|
17
|
+
SubscriptionUpdate,
|
|
18
|
+
TemplateCreate,
|
|
19
|
+
TemplateRead,
|
|
20
|
+
TemplateUpdate,
|
|
21
|
+
)
|
|
22
|
+
from ._db import commit_or_name_conflict
|
|
23
|
+
|
|
24
|
+
router = APIRouter(tags=['config'])
|
|
25
|
+
DbSession = Annotated[AsyncSession, Depends(get_db_session)]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _ensure_template_exists(db: AsyncSession, template_id: int | None) -> None:
|
|
29
|
+
if template_id is None:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
template = await db.get(Template, template_id)
|
|
33
|
+
if template is None:
|
|
34
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='template not found')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get('/subscriptions', response_model=list[SubscriptionRead])
|
|
38
|
+
async def list_subscriptions(db: DbSession) -> list[Subscription]:
|
|
39
|
+
return list((await db.scalars(select(Subscription).order_by(Subscription.id))).all())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.post('/subscriptions', response_model=SubscriptionRead, status_code=status.HTTP_201_CREATED)
|
|
43
|
+
async def create_subscription(payload: SubscriptionCreate, db: DbSession) -> Subscription:
|
|
44
|
+
await _ensure_template_exists(db, payload.template_id)
|
|
45
|
+
subscription = Subscription(
|
|
46
|
+
name=payload.name,
|
|
47
|
+
url=str(payload.url) if payload.url is not None else None,
|
|
48
|
+
content=payload.content,
|
|
49
|
+
proxy=payload.proxy,
|
|
50
|
+
headers=payload.headers,
|
|
51
|
+
follow_redirects=payload.follow_redirects,
|
|
52
|
+
enabled=payload.enabled,
|
|
53
|
+
template_id=payload.template_id,
|
|
54
|
+
)
|
|
55
|
+
db.add(subscription)
|
|
56
|
+
await commit_or_name_conflict(db, resource_name='subscription', table_name='subscriptions')
|
|
57
|
+
await db.refresh(subscription)
|
|
58
|
+
return subscription
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get('/subscriptions/{subscription_id}', response_model=SubscriptionRead)
|
|
62
|
+
async def get_subscription(subscription_id: int, db: DbSession) -> Subscription:
|
|
63
|
+
subscription = await db.get(Subscription, subscription_id)
|
|
64
|
+
if subscription is None:
|
|
65
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='subscription not found')
|
|
66
|
+
return subscription
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.put('/subscriptions/{subscription_id}', response_model=SubscriptionRead)
|
|
70
|
+
async def update_subscription(subscription_id: int, payload: SubscriptionUpdate, db: DbSession) -> Subscription:
|
|
71
|
+
subscription = await db.get(Subscription, subscription_id)
|
|
72
|
+
if subscription is None:
|
|
73
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='subscription not found')
|
|
74
|
+
|
|
75
|
+
current = {
|
|
76
|
+
'name': subscription.name,
|
|
77
|
+
'url': subscription.url,
|
|
78
|
+
'content': subscription.content,
|
|
79
|
+
'proxy': subscription.proxy,
|
|
80
|
+
'headers': subscription.headers,
|
|
81
|
+
'follow_redirects': subscription.follow_redirects,
|
|
82
|
+
'enabled': subscription.enabled,
|
|
83
|
+
'template_id': subscription.template_id,
|
|
84
|
+
}
|
|
85
|
+
updated = payload.model_dump(exclude_unset=True)
|
|
86
|
+
merged = current | updated
|
|
87
|
+
SubscriptionConfig.model_validate(
|
|
88
|
+
{
|
|
89
|
+
'name': merged['name'],
|
|
90
|
+
'url': merged['url'],
|
|
91
|
+
'content': merged['content'],
|
|
92
|
+
'proxy': merged['proxy'],
|
|
93
|
+
'headers': merged['headers'],
|
|
94
|
+
'follow_redirects': merged['follow_redirects'],
|
|
95
|
+
'enabled': merged['enabled'],
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
await _ensure_template_exists(db, merged['template_id'])
|
|
99
|
+
|
|
100
|
+
for field, value in merged.items():
|
|
101
|
+
if field == 'url' and value is not None:
|
|
102
|
+
setattr(subscription, field, str(value))
|
|
103
|
+
else:
|
|
104
|
+
setattr(subscription, field, value)
|
|
105
|
+
|
|
106
|
+
await commit_or_name_conflict(db, resource_name='subscription', table_name='subscriptions')
|
|
107
|
+
await db.refresh(subscription)
|
|
108
|
+
return subscription
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.delete('/subscriptions/{subscription_id}', status_code=status.HTTP_204_NO_CONTENT)
|
|
112
|
+
async def delete_subscription(subscription_id: int, db: DbSession) -> Response:
|
|
113
|
+
subscription = await db.get(Subscription, subscription_id)
|
|
114
|
+
if subscription is None:
|
|
115
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='subscription not found')
|
|
116
|
+
await db.delete(subscription)
|
|
117
|
+
await db.commit()
|
|
118
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.get('/templates', response_model=list[TemplateRead])
|
|
122
|
+
async def list_templates(db: DbSession) -> list[Template]:
|
|
123
|
+
return list((await db.scalars(select(Template).order_by(Template.id))).all())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@router.post('/templates', response_model=TemplateRead, status_code=status.HTTP_201_CREATED)
|
|
127
|
+
async def create_template(payload: TemplateCreate, db: DbSession) -> Template:
|
|
128
|
+
template = Template(name=payload.name, content=payload.content, is_default=payload.is_default)
|
|
129
|
+
db.add(template)
|
|
130
|
+
await commit_or_name_conflict(db, resource_name='template', table_name='templates')
|
|
131
|
+
await db.refresh(template)
|
|
132
|
+
return template
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.get('/templates/{template_id}', response_model=TemplateRead)
|
|
136
|
+
async def get_template(template_id: int, db: DbSession) -> Template:
|
|
137
|
+
template = await db.get(Template, template_id)
|
|
138
|
+
if template is None:
|
|
139
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='template not found')
|
|
140
|
+
return template
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.put('/templates/{template_id}', response_model=TemplateRead)
|
|
144
|
+
async def update_template(template_id: int, payload: TemplateUpdate, db: DbSession) -> Template:
|
|
145
|
+
template = await db.get(Template, template_id)
|
|
146
|
+
if template is None:
|
|
147
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='template not found')
|
|
148
|
+
for field, value in payload.model_dump(exclude_unset=True).items():
|
|
149
|
+
setattr(template, field, value)
|
|
150
|
+
await commit_or_name_conflict(db, resource_name='template', table_name='templates')
|
|
151
|
+
await db.refresh(template)
|
|
152
|
+
return template
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@router.delete('/templates/{template_id}', status_code=status.HTTP_204_NO_CONTENT)
|
|
156
|
+
async def delete_template(template_id: int, db: DbSession) -> Response:
|
|
157
|
+
template = await db.get(Template, template_id)
|
|
158
|
+
if template is None:
|
|
159
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='template not found')
|
|
160
|
+
|
|
161
|
+
composite_template = await db.scalar(
|
|
162
|
+
select(CompositeTemplate.id).where(CompositeTemplate.base_template_id == template_id)
|
|
163
|
+
)
|
|
164
|
+
if composite_template is not None:
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
167
|
+
detail='template is used by a composite template',
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
await db.delete(template)
|
|
171
|
+
await db.commit()
|
|
172
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|