Compare commits

..

11 Commits

21 changed files with 340 additions and 136 deletions

View File

@ -13,9 +13,10 @@ A Python package for surfing internet scientifically.
### 1. Clone into local ### 1. Clone into local
```bash ```bash
git clone https://github.com/klesh/scientific-surfing.git git clone ssh://git@gitea.epss.net.cn:2223/klesh/ss.git
cd scientific-surfing cd ss
poetry install brew install poetry
poetry config --local virtualenvs.in-project true
``` ```
### 2. Add the root directory to system PATH ### 2. Add the root directory to system PATH
@ -25,74 +26,77 @@ poetry install
### Subscription Management ### Subscription Management
```bash ```bash
# add a subscription # add a subscription
python -m scientific_surfing subscription add <name> <clash-rss-subscription-url> python -m ss subscription add <name> <clash-rss-subscription-url>
# refresh a subscription # refresh a subscription
python -m scientific_surfing subscription refresh <name> python -m ss subscription refresh <name>
# delete a subscription # delete a subscription
python -m scientific_surfing subscription rm <name> python -m ss subscription rm <name>
# rename a subscription # rename a subscription
python -m scientific_surfing subscription rename <name> <new-name> python -m ss subscription rename <name> <new-name>
# update subscription URL
python -m ss subscription set-url <name> <new-url>
# activate a subscription # activate a subscription
python -m scientific_surfing subscription activate <name> python -m ss subscription activate <name>
# list all subscriptions # list all subscriptions
python -m scientific_surfing subscription list python -m ss subscription list
# show storage information # show storage information
python -m scientific_surfing subscription storage python -m ss subscription storage
``` ```
### Hook Management ### Hook Management
```bash ```bash
# initialize hooks directory with template scripts # initialize hooks directory with template scripts
python -m scientific_surfing hook init python -m ss hook init
# show hooks directory location and list all scripts # show hooks directory location and list all scripts
python -m scientific_surfing hook list python -m ss hook list
# edit a hook script with system editor # edit a hook script with system editor
python -m scientific_surfing hook edit <script-name> python -m ss hook edit <script-name>
# remove a hook script # remove a hook script
python -m scientific_surfing hook rm <script-name> python -m ss hook rm <script-name>
``` ```
### Core Configuration Management ### Core Configuration Management
```bash ```bash
# import configuration from file # import configuration from file
python -m scientific_surfing core-config import <file-path> python -m ss core config import <file-path>
# export configuration to file # export configuration to file
python -m scientific_surfing core-config export <file-path> python -m ss core config export <file-path>
# edit configuration with system editor # edit configuration with system editor
python -m scientific_surfing core-config edit python -m ss core config edit
# reset configuration to default values # reset configuration to default values
python -m scientific_surfing core-config reset python -m ss core config reset
# show current configuration # show current configuration
python -m scientific_surfing core-config show python -m ss core config show
# apply active subscription to generate final config # apply active subscription to generate final config
python -m scientific_surfing core-config apply python -m ss core config apply
``` ```
### Core Management ### Core Management
```bash ```bash
# update scientific-surfing core components # update scientific-surfing core components
python -m scientific_surfing core update [--version <version>] [--force] python -m ss core update [--version <version>] [--force]
``` ```
### Service Management ### Service Management
macOS Linux / macOS
```bash ```nushell
sudo env SF_CONFIG_DIR=(readlink -f ~/basicfiles/cli/scientific_surfing) python -m scientific_surfing core service install sudo env SF_CONFIG_DIR=$HOME/basicfiles/cli/ss python3 -m ss core service install
``` ```
## Development ## Development

202
poetry.lock generated
View File

@ -83,7 +83,10 @@ mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0,<1" pathspec = ">=0.9.0,<1"
platformdirs = ">=2" platformdirs = ">=2"
tomli = ">=0.2.6,<2.0.0" tomli = ">=0.2.6,<2.0.0"
typing-extensions = {version = ">=3.10.0.0,<3.10.0.1 || >3.10.0.1", markers = "python_version >= \"3.10\""} typing-extensions = [
{version = ">=3.10.0.0,<3.10.0.1 || >3.10.0.1", markers = "python_version >= \"3.10\""},
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
]
[package.extras] [package.extras]
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
@ -325,6 +328,22 @@ files = [
{file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"},
] ]
[[package]]
name = "click"
version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version == \"3.9\""
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.0" version = "8.3.0"
@ -332,6 +351,7 @@ description = "Composable command line interface toolkit"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["dev"] groups = ["dev"]
markers = "python_version >= \"3.10\""
files = [ files = [
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
@ -544,7 +564,7 @@ description = "Backport of PEP 654 (exception groups)"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
markers = "python_version == \"3.10\"" markers = "python_version < \"3.11\""
files = [ files = [
{file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
{file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
@ -603,6 +623,31 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version == \"3.9\""
files = [
{file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"},
{file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"},
]
[package.dependencies]
zipp = ">=3.20"
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=3.4)"]
perf = ["ipython"]
test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.1.0"
@ -617,14 +662,14 @@ files = [
[[package]] [[package]]
name = "ipykernel" name = "ipykernel"
version = "7.0.1" version = "6.31.0"
description = "IPython Kernel for Jupyter" description = "IPython Kernel for Jupyter"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "ipykernel-7.0.1-py3-none-any.whl", hash = "sha256:87182a8305e28954b6721087dec45b171712610111d494c17bb607befa1c4000"}, {file = "ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af"},
{file = "ipykernel-7.0.1.tar.gz", hash = "sha256:2d3fd7cdef22071c2abbad78f142b743228c5d59cd470d034871ae0ac359533c"}, {file = "ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6"},
] ]
[package.dependencies] [package.dependencies]
@ -644,11 +689,50 @@ traitlets = ">=5.4.0"
[package.extras] [package.extras]
cov = ["coverage[toml]", "matplotlib", "pytest-cov", "trio"] cov = ["coverage[toml]", "matplotlib", "pytest-cov", "trio"]
docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx (<8.2.0)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"]
pyqt5 = ["pyqt5"] pyqt5 = ["pyqt5"]
pyside6 = ["pyside6"] pyside6 = ["pyside6"]
test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0,<9)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0,<9)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"]
[[package]]
name = "ipython"
version = "8.18.1"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version == \"3.9\""
files = [
{file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"},
{file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
exceptiongroup = {version = "*", markers = "python_version < \"3.11\""}
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
prompt-toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5"
typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
[package.extras]
all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"]
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "8.37.0" version = "8.37.0"
@ -656,6 +740,7 @@ description = "IPython: Productive Interactive Computing"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"] groups = ["main"]
markers = "python_version >= \"3.10\""
files = [ files = [
{file = "ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2"}, {file = "ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2"},
{file = "ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216"}, {file = "ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216"},
@ -721,6 +806,7 @@ files = [
] ]
[package.dependencies] [package.dependencies]
importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""}
jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
python-dateutil = ">=2.8.2" python-dateutil = ">=2.8.2"
pyzmq = ">=23.0" pyzmq = ">=23.0"
@ -731,6 +817,28 @@ traitlets = ">=5.3"
docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
[[package]]
name = "jupyter-core"
version = "5.8.1"
description = "Jupyter core package. A base package on which Jupyter projects rely."
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version == \"3.9\""
files = [
{file = "jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0"},
{file = "jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941"},
]
[package.dependencies]
platformdirs = ">=2.5"
pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
traitlets = ">=5.3"
[package.extras]
docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"]
test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"]
[[package]] [[package]]
name = "jupyter-core" name = "jupyter-core"
version = "5.9.1" version = "5.9.1"
@ -738,6 +846,7 @@ description = "Jupyter core package. A base package on which Jupyter projects re
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"] groups = ["main"]
markers = "python_version >= \"3.10\""
files = [ files = [
{file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"}, {file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"},
{file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"}, {file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"},
@ -849,7 +958,7 @@ description = "Pexpect allows easy control of interactive console applications."
optional = false optional = false
python-versions = "*" python-versions = "*"
groups = ["main"] groups = ["main"]
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or python_version == \"3.9\" and sys_platform != \"win32\""
files = [ files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
@ -858,6 +967,24 @@ files = [
[package.dependencies] [package.dependencies]
ptyprocess = ">=0.5" ptyprocess = ">=0.5"
[[package]]
name = "platformdirs"
version = "4.4.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
markers = "python_version == \"3.9\""
files = [
{file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"},
{file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.14.1)"]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.5.0" version = "4.5.0"
@ -865,6 +992,7 @@ description = "A small Python package for determining appropriate platform-speci
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main", "dev"] groups = ["main", "dev"]
markers = "python_version >= \"3.10\""
files = [ files = [
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
@ -936,7 +1064,7 @@ description = "Run a subprocess in a pseudo terminal"
optional = false optional = false
python-versions = "*" python-versions = "*"
groups = ["main"] groups = ["main"]
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or python_version == \"3.9\" and sys_platform != \"win32\""
files = [ files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
@ -1233,6 +1361,37 @@ files = [
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
[[package]]
name = "pywin32"
version = "311"
description = "Python for Window Extensions"
optional = false
python-versions = "*"
groups = ["main"]
markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\" and python_version == \"3.9\""
files = [
{file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"},
{file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"},
{file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"},
{file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"},
{file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"},
{file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"},
{file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"},
{file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"},
{file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"},
{file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"},
{file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"},
{file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"},
{file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"},
{file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"},
{file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"},
{file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"},
{file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"},
{file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"},
{file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"},
{file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"},
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"
@ -1594,7 +1753,28 @@ files = [
{file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"}, {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"},
] ]
[[package]]
name = "zipp"
version = "3.23.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version == \"3.9\""
files = [
{file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"},
{file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.10" python-versions = "^3.9"
content-hash = "0f4bb95342f36a6a399cb54a8f1ea834eb307d5fc2b2fdd4ff67c9773ffb31fd" content-hash = "820a4e7b4d40f0e3bffdc4d8a3cb5538acc5536f59711754c75f123cc4232f24"

2
poetry.toml Normal file
View File

@ -0,0 +1,2 @@
[virtualenvs]
in-project = true

View File

@ -1,17 +1,17 @@
[tool.poetry] [tool.poetry]
name = "scientific-surfing" name = "ss"
version = "0.1.0" version = "0.1.0"
description = "A Python package for surfing the internet scientifically" description = "A Python package for surfing the internet scientifically"
authors = ["Scientific Surfing Team <team@scientific-surfing.com>"] authors = ["Scientific Surfing Team <team@scientific-surfing.com>"]
readme = "README.md" readme = "README.md"
packages = [{include = "scientific_surfing"}] packages = [{include = "ss"}]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.9"
requests = "^2.25.0" requests = "^2.25.0"
PyYAML = "^6.0.0" PyYAML = "^6.0.0"
pydantic = "^2.0.0" pydantic = "^2.0.0"
ipykernel = "^7.0.1" ipykernel = "^6.31.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^6.0.0" pytest = "^6.0.0"

View File

@ -1,8 +0,0 @@
"""
Entry point for python -m scientific_surfing
"""
from scientific_surfing.cli import main
if __name__ == '__main__':
main()

8
ss/__main__.py Normal file
View File

@ -0,0 +1,8 @@
"""
Entry point for python -m ss
"""
from .cli import main
if __name__ == '__main__':
main()

View File

@ -4,11 +4,11 @@ Command-line interface for scientific-surfing package.
import argparse import argparse
import sys import sys
from scientific_surfing.storage import StorageManager from .storage import StorageManager
from scientific_surfing.subscription_manager import SubscriptionManager from .subscription_manager import SubscriptionManager
from scientific_surfing.corecfg_manager import CoreConfigManager from .corecfg_manager import CoreConfigManager
from scientific_surfing.core_manager import CoreManager from .core_manager import CoreManager
from scientific_surfing.hook_manager import HookManager from .hook_manager import HookManager
def create_parser() -> argparse.ArgumentParser: def create_parser() -> argparse.ArgumentParser:
"""Create the argument parser.""" """Create the argument parser."""
@ -40,6 +40,11 @@ def create_parser() -> argparse.ArgumentParser:
rename_parser.add_argument('name', help='Current name of the subscription') rename_parser.add_argument('name', help='Current name of the subscription')
rename_parser.add_argument('new_name', help='New name for the subscription') rename_parser.add_argument('new_name', help='New name for the subscription')
# Set URL subscription command
set_url_parser = subscription_subparsers.add_parser('set-url', help='Update the URL for a subscription')
set_url_parser.add_argument('name', help='Name of the subscription')
set_url_parser.add_argument('url', help='New URL for the subscription')
# Activate subscription command # Activate subscription command
activate_parser = subscription_subparsers.add_parser('activate', help='Activate a subscription') activate_parser = subscription_subparsers.add_parser('activate', help='Activate a subscription')
activate_parser.add_argument('name', help='Name of the subscription to activate') activate_parser.add_argument('name', help='Name of the subscription to activate')
@ -50,31 +55,6 @@ def create_parser() -> argparse.ArgumentParser:
# Storage info command # Storage info command
storage_parser = subscription_subparsers.add_parser('storage', help='Show storage information') storage_parser = subscription_subparsers.add_parser('storage', help='Show storage information')
# Core config commands
core_config_parser = subparsers.add_parser('core-config', help='Manage core configuration')
core_config_subparsers = core_config_parser.add_subparsers(dest='core_config_command', help='Configuration operations')
# Import config
import_parser = core_config_subparsers.add_parser('import', help='Import configuration from file')
import_parser.add_argument('source', help='Path to configuration file to import')
# Export config
export_parser = core_config_subparsers.add_parser('export', help='Export configuration to file')
export_parser.add_argument('destination', help='Path to save configuration file')
# Edit config
edit_parser = core_config_subparsers.add_parser('edit', help='Edit configuration with system editor')
# Reset config
reset_parser = core_config_subparsers.add_parser('reset', help='Reset configuration to default values')
# Show config
show_parser = core_config_subparsers.add_parser('show', help='Show current configuration')
# Apply config
apply_parser = core_config_subparsers.add_parser('apply', help='Apply active subscription to generate final config')
# Core commands # Core commands
core_parser = subparsers.add_parser('core', help='Manage scientific-surfing core components') core_parser = subparsers.add_parser('core', help='Manage scientific-surfing core components')
core_subparsers = core_parser.add_subparsers(dest='core_command', help='Core operations') core_subparsers = core_parser.add_subparsers(dest='core_command', help='Core operations')
@ -84,6 +64,30 @@ def create_parser() -> argparse.ArgumentParser:
update_parser.add_argument('--version', help='Specific version to download (e.g., v1.18.5). If not specified, downloads latest') update_parser.add_argument('--version', help='Specific version to download (e.g., v1.18.5). If not specified, downloads latest')
update_parser.add_argument('--force', action='store_true', help='Force update even if binary already exists') update_parser.add_argument('--force', action='store_true', help='Force update even if binary already exists')
# Config commands
config_parser = core_subparsers.add_parser('config', help='Manage core configuration')
config_subparsers = config_parser.add_subparsers(dest='config_command', help='Configuration operations')
# Import config
import_parser = config_subparsers.add_parser('import', help='Import configuration from file')
import_parser.add_argument('source', help='Path to configuration file to import')
# Export config
export_parser = config_subparsers.add_parser('export', help='Export configuration to file')
export_parser.add_argument('destination', help='Path to save configuration file')
# Edit config
edit_parser = config_subparsers.add_parser('edit', help='Edit configuration with system editor')
# Reset config
reset_parser = config_subparsers.add_parser('reset', help='Reset configuration to default values')
# Show config
show_parser = config_subparsers.add_parser('show', help='Show current configuration')
# Apply config
apply_parser = config_subparsers.add_parser('apply', help='Apply active subscription to generate final config')
# Service management commands # Service management commands
service_parser = core_subparsers.add_parser('service', help='Manage mihomo as a system service') service_parser = core_subparsers.add_parser('service', help='Manage mihomo as a system service')
service_subparsers = service_parser.add_subparsers(dest='service_command', help='Service operations') service_subparsers = service_parser.add_subparsers(dest='service_command', help='Service operations')
@ -164,6 +168,8 @@ def main() -> None:
subscription_manager.delete_subscription(args.name) subscription_manager.delete_subscription(args.name)
elif args.subcommand == 'rename': elif args.subcommand == 'rename':
subscription_manager.rename_subscription(args.name, args.new_name) subscription_manager.rename_subscription(args.name, args.new_name)
elif args.subcommand == 'set-url':
subscription_manager.set_subscription_url(args.name, args.url)
elif args.subcommand == 'activate': elif args.subcommand == 'activate':
subscription_manager.activate_subscription(args.name) subscription_manager.activate_subscription(args.name)
elif args.subcommand == 'list': elif args.subcommand == 'list':
@ -173,27 +179,6 @@ def main() -> None:
else: else:
parser.parse_args(['subscription', '--help']) parser.parse_args(['subscription', '--help'])
elif args.command == 'core-config':
if not hasattr(args, 'core_config_command') or not args.core_config_command:
parser.parse_args(['core-config', '--help'])
return
if args.core_config_command == 'import':
core_config_manager.import_config(args.source)
elif args.core_config_command == 'export':
core_config_manager.export_config(args.destination)
elif args.core_config_command == 'edit':
core_config_manager.edit_config()
elif args.core_config_command == 'reset':
core_config_manager.reset_config()
elif args.core_config_command == 'show':
core_config_manager.show_config()
elif args.core_config_command == 'apply':
core_config_manager.apply()
else:
parser.parse_args(['core-config', '--help'])
elif args.command == 'core': elif args.command == 'core':
if not hasattr(args, 'core_command') or not args.core_command: if not hasattr(args, 'core_command') or not args.core_command:
parser.parse_args(['core', '--help']) parser.parse_args(['core', '--help'])
@ -201,6 +186,25 @@ def main() -> None:
if args.core_command == 'update': if args.core_command == 'update':
core_manager.update(version=args.version, force=args.force) core_manager.update(version=args.version, force=args.force)
elif args.core_command == 'config':
if not hasattr(args, 'config_command') or not args.config_command:
parser.parse_args(['core', 'config', '--help'])
return
if args.config_command == 'import':
core_config_manager.import_config(args.source)
elif args.config_command == 'export':
core_config_manager.export_config(args.destination)
elif args.config_command == 'edit':
core_config_manager.edit_config()
elif args.config_command == 'reset':
core_config_manager.reset_config()
elif args.config_command == 'show':
core_config_manager.show_config()
elif args.config_command == 'apply':
core_config_manager.apply()
else:
parser.parse_args(['core', 'config', '--help'])
elif args.core_command == 'service': elif args.core_command == 'service':
if not hasattr(args, 'service_command') or not args.service_command: if not hasattr(args, 'service_command') or not args.service_command:
parser.parse_args(['core', 'service', '--help']) parser.parse_args(['core', 'service', '--help'])
@ -262,7 +266,6 @@ def main() -> None:
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"❌ Error: {e}")
raise raise
sys.exit(1)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -14,8 +14,8 @@ from typing import Optional, Dict, Any
import requests import requests
from pathlib import Path from pathlib import Path
from scientific_surfing.corecfg_manager import CoreConfigManager from .corecfg_manager import CoreConfigManager
from scientific_surfing.service_manager import ServiceManager from .service_manager import ServiceManager
class CoreManager: class CoreManager:

View File

@ -11,8 +11,8 @@ from pathlib import Path
import yaml import yaml
from scientific_surfing.models import Config from .models import Config
from scientific_surfing.subscription_manager import SubscriptionManager from .subscription_manager import SubscriptionManager
class CoreConfigManager: class CoreConfigManager:
@ -175,11 +175,7 @@ class CoreConfigManager:
def show_config(self) -> None: def show_config(self) -> None:
"""Display current configuration.""" """Display current configuration."""
config = self.load_config() config = self.load_config()
print("⚙️ Current Configuration:") print(yaml.dump(config, indent=2))
print(f" Auto-refresh: {config.auto_refresh}")
print(f" Refresh interval: {config.refresh_interval_hours} hours")
print(f" User-Agent: {config.default_user_agent}")
print(f" Timeout: {config.timeout_seconds} seconds")
def get_config(self) -> Config: def get_config(self) -> Config:
"""Get current configuration.""" """Get current configuration."""
@ -221,7 +217,7 @@ class CoreConfigManager:
# On Windows, try to execute directly (batch files, etc.) # On Windows, try to execute directly (batch files, etc.)
cmd = [str(hook_path), str(config_file_path)] cmd = [str(hook_path), str(config_file_path)]
print(f"🔧 Executing hook: {hook_path.name}") print(f"🔧 Executing hook: {hook_path}")
env = os.environ.copy() env = os.environ.copy()
env['PYTHONIOENCODING'] = 'utf-8' env['PYTHONIOENCODING'] = 'utf-8'
result = subprocess.run( result = subprocess.run(
@ -231,8 +227,9 @@ class CoreConfigManager:
text=True, text=True,
timeout=30, timeout=30,
encoding="utf-8", encoding="utf-8",
shell=True, # shell=True,
env=env, env=env,
# stdout=subprocess.PIPE, stderr=subprocess.STDOUT
) )
if result.returncode == 0: if result.returncode == 0:
@ -276,7 +273,7 @@ class CoreConfigManager:
def apply(self) -> bool: def apply(self) -> bool:
"""Apply active subscription to generate final config file.""" """Apply active subscription to generate final config file."""
from scientific_surfing.subscription_manager import SubscriptionManager from ss.subscription_manager import SubscriptionManager
# Load current configuration # Load current configuration
config = self.load_config() config = self.load_config()
@ -306,9 +303,9 @@ class CoreConfigManager:
# Create final config by merging subscription with user config # Create final config by merging subscription with user config
final_config = deep_merge(subscription_data, config) final_config = deep_merge(subscription_data, config)
external_ui = final_config.get("external-ui") # external_ui = final_config.get("external-ui")
if external_ui: # if external_ui:
final_config["external-ui"] = os.path.join(self.storage.config_dir, external_ui) # final_config["external-ui"] = os.path.join(self.storage.config_dir, external_ui)
# Define essential defaults that should be present in any Clash config # Define essential defaults that should be present in any Clash config
essential_defaults = { essential_defaults = {

View File

@ -6,7 +6,7 @@ import subprocess
from pathlib import Path from pathlib import Path
from typing import List from typing import List
from scientific_surfing.storage import StorageManager from .storage import StorageManager
class HookManager: class HookManager:

View File

@ -107,3 +107,11 @@ class SubscriptionsData(BaseModel):
subscription.name = new_name subscription.name = new_name
self.subscriptions[new_name] = subscription self.subscriptions[new_name] = subscription
return True return True
def set_subscription_url(self, name: str, url: str) -> bool:
"""Update the URL for a subscription."""
if name not in self.subscriptions:
return False
self.subscriptions[name].url = url
return True

View File

@ -16,7 +16,7 @@ from typing import Optional, Protocol
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from scientific_surfing.storage import StorageManager from .storage import StorageManager
class ServiceConfig(BaseModel): class ServiceConfig(BaseModel):
@ -216,8 +216,6 @@ class WindowsServiceManager(ServiceManagerProtocol):
def uninstall(self, name: str) -> None: def uninstall(self, name: str) -> None:
"""Uninstall a Windows service.""" """Uninstall a Windows service."""
import json
from pathlib import Path
try: try:
# Stop the service first # Stop the service first
@ -231,7 +229,7 @@ class WindowsServiceManager(ServiceManagerProtocol):
self._run_as_admin(["sc", "delete", name], f"uninstall service '{name}'") self._run_as_admin(["sc", "delete", name], f"uninstall service '{name}'")
# Clean up configuration file # Clean up configuration file
config_dir = Path.home() / ".scientific_surfing" / "service_configs" config_dir = self.config_dir
config_file = config_dir / f"{name}_config.json" config_file = config_dir / f"{name}_config.json"
try: try:
config_file.unlink(missing_ok=True) config_file.unlink(missing_ok=True)

View File

@ -9,7 +9,7 @@ import yaml
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from scientific_surfing.models import SubscriptionsData from .models import SubscriptionsData
class StorageManager: class StorageManager:

View File

@ -5,7 +5,7 @@ Handles subscription operations with persistent storage.
from datetime import datetime from datetime import datetime
import requests import requests
from scientific_surfing.storage import StorageManager from .storage import StorageManager
class SubscriptionManager: class SubscriptionManager:
@ -21,12 +21,13 @@ class SubscriptionManager:
self.subscriptions_dir = self.storage.config_dir / "subscriptions" self.subscriptions_dir = self.storage.config_dir / "subscriptions"
self.subscriptions_dir.mkdir(exist_ok=True) self.subscriptions_dir.mkdir(exist_ok=True)
def add_subscription(self, url: str, name: str) -> None: def add_subscription(self, name: str, url: str) -> None:
"""Add a new subscription.""" """Add a new subscription."""
subscription = self.subscriptions_data.add_subscription(name, url) subscription = self.subscriptions_data.add_subscription(name, url)
if self.storage.save_subscriptions(self.subscriptions_data): if self.storage.save_subscriptions(self.subscriptions_data):
self.refresh_subscription(name) self.refresh_subscription(subscription.name)
self.activate_subscription(subscription.name)
print(f"✅ Added subscription: {name} -> {url}") print(f"✅ Added subscription: {name} -> {url}")
else: else:
print("❌ Failed to save subscription") print("❌ Failed to save subscription")
@ -45,11 +46,12 @@ class SubscriptionManager:
try: try:
# Download the subscription content # Download the subscription content
headers = { headers = {
'User-Agent': self.config.default_user_agent # 'User-Agent': self.config.default_user_agent
} }
timeout = self.config.timeout_seconds # timeout = self.config.timeout_seconds
response = requests.get(url, headers=headers, timeout=timeout) # response = requests.get(url, headers=headers, timeout=timeout)
response = requests.get(url)
response.raise_for_status() response.raise_for_status()
# File path without timestamp # File path without timestamp
@ -121,6 +123,16 @@ class SubscriptionManager:
else: else:
print(f"❌ Failed to rename subscription: '{old_name}' not found or '{new_name}' already exists") print(f"❌ Failed to rename subscription: '{old_name}' not found or '{new_name}' already exists")
def set_subscription_url(self, name: str, url: str) -> None:
"""Update the URL for a subscription."""
if self.subscriptions_data.set_subscription_url(name, url):
if self.storage.save_subscriptions(self.subscriptions_data):
print(f"✅ Updated URL for subscription '{name}': {url}")
else:
print("❌ Failed to save subscription")
else:
print(f"❌ Subscription '{name}' not found")
def activate_subscription(self, name: str) -> None: def activate_subscription(self, name: str) -> None:
"""Activate a subscription.""" """Activate a subscription."""
if self.subscriptions_data.set_active(name): if self.subscriptions_data.set_active(name):

View File

@ -7,7 +7,7 @@ import sys
import os import os
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from scientific_surfing.corecfg_manager import CoreConfigManager from ss.corecfg_manager import CoreConfigManager
def test_upgrade(): def test_upgrade():
"""Test the upgrade method functionality.""" """Test the upgrade method functionality."""
@ -72,7 +72,7 @@ def test_upgrade():
return False return False
# Test 4: Test directory creation # Test 4: Test directory creation
from scientific_surfing.storage import StorageManager from ss.storage import StorageManager
storage = StorageManager() storage = StorageManager()
binary_dir = storage.config_dir / "bin" binary_dir = storage.config_dir / "bin"
print(f"Binary directory: {binary_dir}") print(f"Binary directory: {binary_dir}")