aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore162
-rw-r--r--README.md3
-rw-r--r--codehere.bat1
-rw-r--r--pdm.lock254
-rw-r--r--pyproject.toml24
-rw-r--r--src/ipm/__init__.py1
-rw-r--r--src/ipm/__main__.py42
-rw-r--r--src/ipm/api.py51
-rw-r--r--src/ipm/const.py1
-rw-r--r--src/ipm/exceptions.py26
-rw-r--r--src/ipm/logging.py3
-rw-r--r--src/ipm/models/__init__.py0
-rw-r--r--src/ipm/models/ipk.py82
-rw-r--r--src/ipm/typing.py10
-rw-r--r--src/ipm/utils/__init__.py0
-rw-r--r--src/ipm/utils/_freeze.py17
-rw-r--r--src/ipm/utils/freeze.py65
-rw-r--r--src/ipm/utils/hash.py15
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_api.py15
20 files changed, 772 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3a8816c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,162 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm-project.org/#use-with-ide
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..64a9aa0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# IPM
+
+Infini 包管理器
diff --git a/codehere.bat b/codehere.bat
new file mode 100644
index 0000000..50a66f4
--- /dev/null
+++ b/codehere.bat
@@ -0,0 +1 @@
+code .
diff --git a/pdm.lock b/pdm.lock
new file mode 100644
index 0000000..b123817
--- /dev/null
+++ b/pdm.lock
@@ -0,0 +1,254 @@
+# This file is @generated by PDM.
+# It is not intended for manual editing.
+
+[metadata]
+groups = ["default"]
+strategy = ["cross_platform", "inherit_metadata"]
+lock_version = "4.4.1"
+content_hash = "sha256:fd0a3ff5fbf43c9b6344f7072de1848ea7b263df6a1042f3dbfaefd12df45d92"
+
+[[package]]
+name = "certifi"
+version = "2023.11.17"
+requires_python = ">=3.6"
+summary = "Python package for providing Mozilla's CA Bundle."
+groups = ["default"]
+files = [
+ {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
+ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+requires_python = ">=3.7.0"
+summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+groups = ["default"]
+files = [
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+summary = "Cross-platform colored terminal text."
+groups = ["default"]
+marker = "sys_platform == \"win32\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.0"
+requires_python = ">=3.7"
+summary = "Backport of PEP 654 (exception groups)"
+groups = ["default"]
+marker = "python_version < \"3.11\""
+files = [
+ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+]
+
+[[package]]
+name = "idna"
+version = "3.6"
+requires_python = ">=3.5"
+summary = "Internationalized Domain Names in Applications (IDNA)"
+groups = ["default"]
+files = [
+ {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+requires_python = ">=3.7"
+summary = "brain-dead simple config-ini parsing"
+groups = ["default"]
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "loguru"
+version = "0.7.2"
+requires_python = ">=3.5"
+summary = "Python logging made (stupidly) simple"
+groups = ["default"]
+dependencies = [
+ "colorama>=0.3.4; sys_platform == \"win32\"",
+ "win32-setctime>=1.0.0; sys_platform == \"win32\"",
+]
+files = [
+ {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"},
+ {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"},
+]
+
+[[package]]
+name = "multilogging"
+version = "1.0.2"
+requires_python = ">=3"
+summary = "分布式 loguru"
+groups = ["default"]
+dependencies = [
+ "loguru",
+]
+files = [
+ {file = "multilogging-1.0.2-py3-none-any.whl", hash = "sha256:46e86043f797944c012db3d177970dbdc0783b51a660953ec87d456ccdc1f568"},
+ {file = "multilogging-1.0.2.tar.gz", hash = "sha256:33ff509ec690de83670740c33f93f655fb406646efa928a02b998e48ce9dacd7"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+requires_python = ">=3.7"
+summary = "Core utilities for Python packages"
+groups = ["default"]
+files = [
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.3.0"
+requires_python = ">=3.8"
+summary = "plugin and hook calling mechanisms for python"
+groups = ["default"]
+files = [
+ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
+ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
+]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+requires_python = ">=3.7"
+summary = "pytest: simple powerful testing with Python"
+groups = ["default"]
+dependencies = [
+ "colorama; sys_platform == \"win32\"",
+ "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"",
+ "iniconfig",
+ "packaging",
+ "pluggy<2.0,>=0.12",
+ "tomli>=1.0.0; python_version < \"3.11\"",
+]
+files = [
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[[package]]
+name = "requests"
+version = "2.31.0"
+requires_python = ">=3.7"
+summary = "Python HTTP for Humans."
+groups = ["default"]
+dependencies = [
+ "certifi>=2017.4.17",
+ "charset-normalizer<4,>=2",
+ "idna<4,>=2.5",
+ "urllib3<3,>=1.21.1",
+]
+files = [
+ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+ {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+summary = "Python Library for Tom's Obvious, Minimal Language"
+groups = ["default"]
+files = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+requires_python = ">=3.7"
+summary = "A lil' TOML parser"
+groups = ["default"]
+marker = "python_version < \"3.11\""
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.1.0"
+requires_python = ">=3.8"
+summary = "HTTP library with thread-safe connection pooling, file post, and more."
+groups = ["default"]
+files = [
+ {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
+ {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
+]
+
+[[package]]
+name = "win32-setctime"
+version = "1.1.0"
+requires_python = ">=3.5"
+summary = "A small Python utility to set file creation time on Windows"
+groups = ["default"]
+marker = "sys_platform == \"win32\""
+files = [
+ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
+ {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1b0aa5c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,24 @@
+[project]
+name = "ipm"
+version = "0.1.0-alpha.2"
+description = "Infini Package Manager"
+authors = [
+ {name = "苏向夜", email = "fu050409@163.com"},
+]
+dependencies = [
+ "multilogging>=1.0.2",
+ "pytest>=7.4.4",
+ "toml>=0.10.2",
+ "requests>=2.31.0",
+]
+requires-python = ">=3.10"
+readme = "README.md"
+license = {text = "MIT"}
+
+[build-system]
+requires = ["pdm-backend"]
+build-backend = "pdm.backend"
+
+
+[tool.pdm]
+package-type = "library"
diff --git a/src/ipm/__init__.py b/src/ipm/__init__.py
new file mode 100644
index 0000000..061d700
--- /dev/null
+++ b/src/ipm/__init__.py
@@ -0,0 +1 @@
+__author__ = "苏向夜 <fu050409@163.com>"
diff --git a/src/ipm/__main__.py b/src/ipm/__main__.py
new file mode 100644
index 0000000..5d878c6
--- /dev/null
+++ b/src/ipm/__main__.py
@@ -0,0 +1,42 @@
+from .api import install, extract, build
+import argparse
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Infini Package Manager")
+ subparsers = parser.add_subparsers(title="subcommands", dest="command")
+
+ # Install command
+ install_parser = subparsers.add_parser("install", help="Install a package")
+ install_parser.add_argument("uri", help="Path or name of the package")
+ install_parser.add_argument("--index", help="Specify a custom package index")
+
+ # Extract command
+ extract_parser = subparsers.add_parser("extract", help="Extract a package")
+ extract_parser.add_argument(
+ "package", help="Path or name of the package to extract"
+ )
+ extract_parser.add_argument(
+ "--dist",
+ default=".",
+ help="Specify extraction directory (default: current directory)",
+ )
+
+ # Build command
+ build_parser = subparsers.add_parser("build", help="打包 Infini 规则包")
+ build_parser.add_argument(
+ "package", nargs="?", help="Path or name of the package to build", default="."
+ )
+
+ args = parser.parse_args()
+
+ if args.command == "install":
+ install(args.uri, args.index)
+ elif args.command == "extract":
+ extract(args.package, args.dist)
+ elif args.command == "build":
+ build(args.package)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/ipm/api.py b/src/ipm/api.py
new file mode 100644
index 0000000..af91dd5
--- /dev/null
+++ b/src/ipm/api.py
@@ -0,0 +1,51 @@
+from pathlib import Path
+from urllib.parse import urlparse
+from .typing import StrPath
+from .utils import freeze
+from .models.ipk import InfiniPackage
+from .exceptions import FileTypeMismatch
+
+import os
+import requests
+import tempfile
+
+
+def build(source_path: StrPath):
+ freeze.build_ipk(InfiniPackage(source_path))
+
+
+def extract(source_path: StrPath, dist_path: StrPath | None = None):
+ dist_path = (
+ Path(dist_path).resolve() if dist_path else Path(source_path).resolve().parent
+ )
+ freeze.extract_ipk(source_path, dist_path)
+
+
+def install(uri: str | None = "", index: str | None = ""):
+ home = Path.home() / ".ipm" / "src"
+ home.mkdir(parents=True, exist_ok=True)
+
+ if uri:
+ if os.path.isabs(uri):
+ if uri.endswith(".ipk"):
+ extract(Path(uri).resolve(), home)
+ else:
+ raise FileTypeMismatch("文件类型与预期[.ipk]不匹配.")
+ elif urlparse(uri).scheme and urlparse(uri).netloc:
+ ipk_bytes = requests.get(uri).content
+ hash_bytes = requests.get(uri.rstrip("/") + ".hash").content
+
+ temp_dir = tempfile.TemporaryDirectory()
+ temp_path = Path(temp_dir.name).resolve()
+
+ ipk_file = (temp_path / "temp.ipk").open("w+b")
+ ipk_file.write(ipk_bytes)
+ ipk_file.close()
+
+ hash_file = (temp_path / "temp.ipk.hash").open("w+b")
+ hash_file.write(hash_bytes)
+ hash_file.close()
+
+ extract(ipk_file, home)
+ else:
+ raise FileTypeMismatch("URI指向未知的位置.")
diff --git a/src/ipm/const.py b/src/ipm/const.py
new file mode 100644
index 0000000..51c64dd
--- /dev/null
+++ b/src/ipm/const.py
@@ -0,0 +1 @@
+DEBUG = True
diff --git a/src/ipm/exceptions.py b/src/ipm/exceptions.py
new file mode 100644
index 0000000..e6a01c0
--- /dev/null
+++ b/src/ipm/exceptions.py
@@ -0,0 +1,26 @@
+class IpmException(Exception):
+ """IPM Base Exception"""
+
+
+class FileNotFoundError(IpmException, FileNotFoundError):
+ """Raises when file not founded"""
+
+
+class FileExistsError(IpmException, FileExistsError):
+ """Raises when file not founded"""
+
+
+class SyntaxError(IpmException, SyntaxError):
+ """Syntax Error in config file"""
+
+
+class HashException(IpmException):
+ """Exception occured in hashing"""
+
+
+class VerifyFailed(IpmException):
+ """Failed to verify ipk file"""
+
+
+class FileTypeMismatch(IpmException):
+ """Ipk file type mismatch"""
diff --git a/src/ipm/logging.py b/src/ipm/logging.py
new file mode 100644
index 0000000..e2321dd
--- /dev/null
+++ b/src/ipm/logging.py
@@ -0,0 +1,3 @@
+from multilogging import multilogger
+
+logger = multilogger(name="IPM")
diff --git a/src/ipm/models/__init__.py b/src/ipm/models/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ipm/models/__init__.py
diff --git a/src/ipm/models/ipk.py b/src/ipm/models/ipk.py
new file mode 100644
index 0000000..14dca06
--- /dev/null
+++ b/src/ipm/models/ipk.py
@@ -0,0 +1,82 @@
+from pathlib import Path
+from ..typing import List, Dict, Literal
+from ..exceptions import SyntaxError
+
+import toml
+
+
+class Author:
+ name: str
+ email: str
+
+ def __init__(self, name: str, email: str) -> None:
+ self.name = name
+ self.email = email
+
+
+class Authors:
+ authors: list[Author] = []
+
+ def __init__(self, authors: List[Dict[Literal["name", "email"], str]]) -> None:
+ for author in authors:
+ self.authors.append(Author(author["name"], author["email"]))
+
+ @property
+ def first(self) -> Author | None:
+ return None if not self.authors else self.authors[0]
+
+
+class InfiniPackage:
+ source_path: Path
+
+ name: str
+ version: str
+ description: str
+ authors: Authors
+ license: str
+
+ def __init__(self, path: str | Path = ".") -> None:
+ self.source_path = Path(path).resolve()
+ toml_path = self.source_path / "infini.toml"
+
+ data_load = toml.load(toml_path.open("r", encoding="utf-8"))
+ if "infini" not in data_load.keys():
+ raise SyntaxError("配置文件中缺少[infini]项.")
+
+ infini: dict = data_load["infini"]
+ self.name = infini.get("name") or ""
+ self.version = infini.get("version") or ""
+ self.description = infini.get("description") or ""
+ self.authors = Authors(infini.get("authors") or [])
+ self.license = infini.get("license") or "MIT"
+
+ @property
+ def default_name(self) -> str:
+ return f"{self.name}-{self.version}.ipk"
+
+ @property
+ def hash_name(self) -> str:
+ return f"{self.name}-{self.version}.ipk.hash"
+
+
+class InfiniFrozenPackage:
+ source_path: Path
+
+ name: str
+ version: str
+ description: str
+ authors: Authors
+ license: str
+
+ def __init__(self, source_path: str | Path, **kwargs) -> None:
+ self.source_path = Path(source_path).resolve()
+
+ self.name = kwargs.get("name") or ""
+ self.version = kwargs.get("version") or ""
+ self.description = kwargs.get("description") or ""
+ self.authors = Authors(kwargs.get("authors") or [])
+ self.license = kwargs.get("license") or "MIT"
+
+ @property
+ def hash_name(self) -> str:
+ return f"{self.source_path.name}.hash"
diff --git a/src/ipm/typing.py b/src/ipm/typing.py
new file mode 100644
index 0000000..dbf124a
--- /dev/null
+++ b/src/ipm/typing.py
@@ -0,0 +1,10 @@
+from pathlib import Path
+from typing import (
+ Literal as Literal,
+ List as List,
+ Dict as Dict,
+ Any as Any,
+ AnyStr as AnyStr,
+)
+
+StrPath = str | Path
diff --git a/src/ipm/utils/__init__.py b/src/ipm/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ipm/utils/__init__.py
diff --git a/src/ipm/utils/_freeze.py b/src/ipm/utils/_freeze.py
new file mode 100644
index 0000000..d6d5791
--- /dev/null
+++ b/src/ipm/utils/_freeze.py
@@ -0,0 +1,17 @@
+from ..logging import logger
+
+import tarfile
+import shutil
+
+
+def create_tar_gz(source_folder: str, output_filepath: str):
+ shutil.move(
+ shutil.make_archive(output_filepath + ".build", "gztar", source_folder),
+ output_filepath,
+ shutil.copy2,
+ )
+
+
+def extract_tar_gz(input_filename: str, output_folder: str):
+ with tarfile.open(input_filename, "r:gz") as tar:
+ tar.extractall(output_folder)
diff --git a/src/ipm/utils/freeze.py b/src/ipm/utils/freeze.py
new file mode 100644
index 0000000..6fd454f
--- /dev/null
+++ b/src/ipm/utils/freeze.py
@@ -0,0 +1,65 @@
+from pathlib import Path
+from . import _freeze
+from ..exceptions import FileNotFoundError, VerifyFailed
+from ..models.ipk import InfiniPackage, InfiniFrozenPackage
+from .hash import hash_ifp, verify_ifp
+from ..typing import StrPath
+
+import tempfile
+import shutil
+
+
+def build_ipk(
+ ipk: InfiniPackage,
+) -> InfiniFrozenPackage:
+ build_dir = ipk.source_path / ".build"
+ src_path = ipk.source_path / "src"
+ dist_path = ipk.source_path / "dist"
+ ifp_path = dist_path / ipk.default_name
+
+ if not ipk.source_path.exists():
+ raise FileNotFoundError(f"文件或文件夹[{ipk.source_path.resolve()}]不存在!")
+ if build_dir.exists():
+ shutil.rmtree(build_dir, ignore_errors=True)
+
+ dist_path.mkdir(parents=True, exist_ok=True)
+ build_dir.mkdir(parents=True, exist_ok=True)
+
+ shutil.copytree(src_path, build_dir / "src")
+ shutil.copy2(ipk.source_path / "infini.toml", build_dir / "infini.toml")
+
+ _freeze.create_tar_gz(
+ str(build_dir),
+ str(ifp_path),
+ )
+
+ (dist_path / ipk.hash_name).write_bytes(hash_ifp(ifp_path))
+
+ return InfiniFrozenPackage(source_path=ifp_path, **{"name": ipk.name})
+
+
+def extract_ipk(source_path: StrPath, dist_path: str | Path) -> InfiniPackage:
+ ifp_path = Path(source_path).resolve()
+ dist_path = Path(dist_path).resolve()
+ hash_path = ifp_path.parent / (ifp_path.name + ".hash")
+
+ if not hash_path.exists():
+ raise VerifyFailed("哈希文件不存在!")
+
+ if not verify_ifp(ifp_path, hash_path.read_bytes()):
+ raise VerifyFailed("文件完整性验证失败!")
+
+ temp_dir = tempfile.TemporaryDirectory()
+
+ temp_path = Path(temp_dir.name).resolve() / "ifp"
+ _freeze.extract_tar_gz(str(ifp_path), str(temp_path))
+ temp_pkg = InfiniPackage(temp_path)
+ dist_pkg_path = dist_path / temp_pkg.name
+
+ if dist_pkg_path.exists():
+ shutil.rmtree(dist_pkg_path)
+
+ shutil.move(temp_path, dist_pkg_path)
+
+ temp_dir.cleanup()
+ return InfiniPackage(dist_pkg_path)
diff --git a/src/ipm/utils/hash.py b/src/ipm/utils/hash.py
new file mode 100644
index 0000000..efd66ca
--- /dev/null
+++ b/src/ipm/utils/hash.py
@@ -0,0 +1,15 @@
+from pathlib import Path
+import hashlib
+
+
+def hash_ifp(lfp_path: str | Path, block_size=65536) -> bytes:
+ sha256 = hashlib.sha256()
+ with Path(lfp_path).resolve().open("rb") as file:
+ for block in iter(lambda: file.read(block_size), b""):
+ sha256.update(block)
+ return sha256.digest()
+
+
+def verify_ifp(lfp_path, expected_hash):
+ actual_hash = hash_ifp(lfp_path)
+ return actual_hash == expected_hash
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 0000000..e45bb77
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,15 @@
+from ipm.api import build, extract, install
+
+
+def test_build():
+ build("C:\\Users\\fu050\\Desktop\\coc")
+
+
+def test_extract():
+ build("C:\\Users\\fu050\\Desktop\\coc")
+ extract("C:\\Users\\fu050\\Desktop\\coc\\dist\\coc-0.1.0-alpha.1.ipk")
+
+
+def test_install():
+ build("C:\\Users\\fu050\\Desktop\\coc")
+ install("C:\\Users\\fu050\\Desktop\\coc\\dist\\coc-0.1.0-alpha.1.ipk")