From a0eced157f1acd3d57b57a7e92a2dd2bb8c7bc24 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Tue, 16 Jan 2024 19:27:02 +0800 Subject: :tada: feat(milestone) IPM 立项 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 162 +++++++++++++++++++++++++++++ README.md | 3 + codehere.bat | 1 + pdm.lock | 254 +++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 24 +++++ src/ipm/__init__.py | 1 + src/ipm/__main__.py | 42 ++++++++ src/ipm/api.py | 51 +++++++++ src/ipm/const.py | 1 + src/ipm/exceptions.py | 26 +++++ src/ipm/logging.py | 3 + src/ipm/models/__init__.py | 0 src/ipm/models/ipk.py | 82 +++++++++++++++ src/ipm/typing.py | 10 ++ src/ipm/utils/__init__.py | 0 src/ipm/utils/_freeze.py | 17 +++ src/ipm/utils/freeze.py | 65 ++++++++++++ src/ipm/utils/hash.py | 15 +++ tests/__init__.py | 0 tests/test_api.py | 15 +++ 20 files changed, 772 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 codehere.bat create mode 100644 pdm.lock create mode 100644 pyproject.toml create mode 100644 src/ipm/__init__.py create mode 100644 src/ipm/__main__.py create mode 100644 src/ipm/api.py create mode 100644 src/ipm/const.py create mode 100644 src/ipm/exceptions.py create mode 100644 src/ipm/logging.py create mode 100644 src/ipm/models/__init__.py create mode 100644 src/ipm/models/ipk.py create mode 100644 src/ipm/typing.py create mode 100644 src/ipm/utils/__init__.py create mode 100644 src/ipm/utils/_freeze.py create mode 100644 src/ipm/utils/freeze.py create mode 100644 src/ipm/utils/hash.py create mode 100644 tests/__init__.py create mode 100644 tests/test_api.py 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__ = "苏向夜 " 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 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 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 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") -- cgit v1.2.3-70-g09d2