├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── changelogithub.config.json ├── pdm.lock ├── pyproject.toml ├── src └── pdm_download │ ├── __init__.py │ └── command.py └── tests ├── __init__.py ├── packages ├── certifi-2023.11.17-py3-none-any.whl ├── certifi-2023.11.17.tar.gz ├── chardet-3.0.4-py2.py3-none-any.whl ├── chardet-3.0.4.tar.gz ├── idna-2.10-py2.py3-none-any.whl ├── idna-2.10.tar.gz ├── requests-2.24.0-py2.py3-none-any.whl ├── requests-2.24.0.tar.gz ├── urllib3-1.25.11-py2.py3-none-any.whl └── urllib3-1.25.11.tar.gz ├── project ├── pdm.lock ├── pdm.static.lock └── pyproject.toml └── test_download.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "*.md" 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - "*.md" 12 | 13 | jobs: 14 | Testing: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up PDM 23 | uses: pdm-project/setup-pdm@v3 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | cache: "true" 27 | 28 | - name: Install packages 29 | run: pdm install 30 | 31 | - name: Run Tests 32 | run: pdm run pytest 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | name: release-pypi 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | 21 | - run: npx changelogithub 22 | continue-on-error: true 23 | env: 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | - uses: actions/setup-python@v4 26 | with: 27 | python-version: "3.11" 28 | - name: Build artifacts 29 | run: pipx run build 30 | - name: Store the distribution packages 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: python-package-distributions 34 | path: dist/ 35 | 36 | pypi-publish: 37 | name: Upload release to PyPI 38 | runs-on: ubuntu-latest 39 | needs: build 40 | environment: 41 | name: pypi 42 | url: https://pypi.org/p/pkg-logical 43 | permissions: 44 | id-token: write 45 | steps: 46 | - name: Download all the dists 47 | uses: actions/download-artifact@v3 48 | with: 49 | name: python-package-distributions 50 | path: dist/ 51 | - name: Publish package distributions to PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm-project.org/#use-with-ide 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Frost Ming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pdm-download 2 | 3 | A PDM plugin to download all packages in a lockfile for offline use. 4 | 5 | 6 | ## Installation 7 | 8 | ```bash 9 | pdm self add pdm-download 10 | ``` 11 | 12 | ## Usage 13 | 14 | This plugin adds a new command `pdm download`, with the following options: 15 | 16 | ```bash 17 | pdm download --help 18 | Usage: pdm download [-h] [-L LOCKFILE] [-v | -q] [-g] [-p PROJECT_PATH] [-d DEST] 19 | 20 | Download all packages from a lockfile for offline use 21 | 22 | Options: 23 | -h, --help Show this help message and exit. 24 | -L LOCKFILE, --lockfile LOCKFILE 25 | Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE] 26 | -v, --verbose Use `-v` for detailed output and `-vv` for more detailed 27 | -q, --quiet Suppress output 28 | -g, --global Use the global project, supply the project root with `-p` option 29 | -p PROJECT_PATH, --project PROJECT_PATH 30 | Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT] 31 | -d DEST, --dest DEST The destination directory, default to './packages' 32 | --python PYTHON Download packages for the given Python range. E.g. '>=3.9' 33 | --platform PLATFORM Download packages for the given platform. E.g. 'linux' 34 | --implementation IMPLEMENTATION 35 | Download packages for the given implementation. E.g. 'cpython', 'pypy' 36 | ``` 37 | -------------------------------------------------------------------------------- /changelogithub.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "break": { "title": "💥 Breaking Changes" }, 4 | "feat": { "title": "🚀 Features" }, 5 | "fix": { "title": "🐞 Bug Fixes" }, 6 | "doc": { "title": "📝 Documentation" }, 7 | "chore": { "title": "💻 Chores" } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "dev"] 6 | strategy = ["inherit_metadata"] 7 | lock_version = "4.5.0" 8 | content_hash = "sha256:efaa6e1273ae008063a1b942357470ad87998738f329faedaaa76105aa2ab6f4" 9 | 10 | [[metadata.targets]] 11 | requires_python = ">=3.8" 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.3.0" 16 | requires_python = ">=3.8" 17 | summary = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | groups = ["dev"] 19 | dependencies = [ 20 | "exceptiongroup>=1.0.2; python_version < \"3.11\"", 21 | "idna>=2.8", 22 | "sniffio>=1.1", 23 | "typing-extensions>=4.1; python_version < \"3.11\"", 24 | ] 25 | files = [ 26 | {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, 27 | {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, 28 | ] 29 | 30 | [[package]] 31 | name = "blinker" 32 | version = "1.7.0" 33 | requires_python = ">=3.8" 34 | summary = "Fast, simple object-to-object and broadcast signaling" 35 | groups = ["dev"] 36 | files = [ 37 | {file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"}, 38 | {file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"}, 39 | ] 40 | 41 | [[package]] 42 | name = "certifi" 43 | version = "2023.11.17" 44 | requires_python = ">=3.6" 45 | summary = "Python package for providing Mozilla's CA Bundle." 46 | groups = ["dev"] 47 | files = [ 48 | {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, 49 | {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, 50 | ] 51 | 52 | [[package]] 53 | name = "colorama" 54 | version = "0.4.6" 55 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 56 | summary = "Cross-platform colored terminal text." 57 | groups = ["dev"] 58 | marker = "sys_platform == \"win32\"" 59 | files = [ 60 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 61 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 62 | ] 63 | 64 | [[package]] 65 | name = "dep-logic" 66 | version = "0.4.2" 67 | requires_python = ">=3.8" 68 | summary = "Python dependency specifications supporting logical operations" 69 | groups = ["dev"] 70 | dependencies = [ 71 | "packaging>=22", 72 | ] 73 | files = [ 74 | {file = "dep_logic-0.4.2-py3-none-any.whl", hash = "sha256:37a668add3f66a13e8a2f6511fac871ce3cc40b01c7fa4b1db23eca626b3549a"}, 75 | {file = "dep_logic-0.4.2.tar.gz", hash = "sha256:c2f6e938ec30788952ee3e0c51da90d043e0354460c96b4fa608ac43a5ce566f"}, 76 | ] 77 | 78 | [[package]] 79 | name = "distlib" 80 | version = "0.3.8" 81 | summary = "Distribution utilities" 82 | groups = ["dev"] 83 | files = [ 84 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 85 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 86 | ] 87 | 88 | [[package]] 89 | name = "exceptiongroup" 90 | version = "1.2.0" 91 | requires_python = ">=3.7" 92 | summary = "Backport of PEP 654 (exception groups)" 93 | groups = ["dev"] 94 | marker = "python_version < \"3.11\"" 95 | files = [ 96 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 97 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 98 | ] 99 | 100 | [[package]] 101 | name = "filelock" 102 | version = "3.13.1" 103 | requires_python = ">=3.8" 104 | summary = "A platform independent file lock." 105 | groups = ["dev"] 106 | files = [ 107 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 108 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 109 | ] 110 | 111 | [[package]] 112 | name = "findpython" 113 | version = "0.6.1" 114 | requires_python = ">=3.8" 115 | summary = "A utility to find python versions on your system" 116 | groups = ["dev"] 117 | dependencies = [ 118 | "packaging>=20", 119 | ] 120 | files = [ 121 | {file = "findpython-0.6.1-py3-none-any.whl", hash = "sha256:1fb4d709205de185b0561900267dfff64a841c910fe28d6038b2394ff925a81a"}, 122 | {file = "findpython-0.6.1.tar.gz", hash = "sha256:56e52b409a92bcbd495cf981c85acf137f3b3e51cc769b46eba219bb1ab7533c"}, 123 | ] 124 | 125 | [[package]] 126 | name = "h11" 127 | version = "0.14.0" 128 | requires_python = ">=3.7" 129 | summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 130 | groups = ["dev"] 131 | dependencies = [ 132 | "typing-extensions; python_version < \"3.8\"", 133 | ] 134 | files = [ 135 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 136 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 137 | ] 138 | 139 | [[package]] 140 | name = "hishel" 141 | version = "0.0.26" 142 | requires_python = ">=3.8" 143 | summary = "Persistent cache implementation for httpx and httpcore" 144 | groups = ["dev"] 145 | dependencies = [ 146 | "httpx>=0.22.0", 147 | "typing-extensions>=4.8.0", 148 | ] 149 | files = [ 150 | {file = "hishel-0.0.26-py3-none-any.whl", hash = "sha256:63cd3fee495124e0ed3461b374c43eab3e9deb2f01ba72eafe36d7df5fcf8b46"}, 151 | {file = "hishel-0.0.26.tar.gz", hash = "sha256:f0ae2766214499cb0253a5ec7694f0d6e3835c9a35634356f8926fb7a1cf379e"}, 152 | ] 153 | 154 | [[package]] 155 | name = "httpcore" 156 | version = "1.0.5" 157 | requires_python = ">=3.8" 158 | summary = "A minimal low-level HTTP client." 159 | groups = ["dev"] 160 | dependencies = [ 161 | "certifi", 162 | "h11<0.15,>=0.13", 163 | ] 164 | files = [ 165 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 166 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 167 | ] 168 | 169 | [[package]] 170 | name = "httpx" 171 | version = "0.27.0" 172 | requires_python = ">=3.8" 173 | summary = "The next generation HTTP client." 174 | groups = ["dev"] 175 | dependencies = [ 176 | "anyio", 177 | "certifi", 178 | "httpcore==1.*", 179 | "idna", 180 | "sniffio", 181 | ] 182 | files = [ 183 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 184 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 185 | ] 186 | 187 | [[package]] 188 | name = "httpx" 189 | version = "0.27.0" 190 | extras = ["socks"] 191 | requires_python = ">=3.8" 192 | summary = "The next generation HTTP client." 193 | groups = ["dev"] 194 | dependencies = [ 195 | "httpx==0.27.0", 196 | "socksio==1.*", 197 | ] 198 | files = [ 199 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 200 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 201 | ] 202 | 203 | [[package]] 204 | name = "idna" 205 | version = "3.6" 206 | requires_python = ">=3.5" 207 | summary = "Internationalized Domain Names in Applications (IDNA)" 208 | groups = ["dev"] 209 | files = [ 210 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 211 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 212 | ] 213 | 214 | [[package]] 215 | name = "importlib-metadata" 216 | version = "7.0.0" 217 | requires_python = ">=3.8" 218 | summary = "Read metadata from Python packages" 219 | groups = ["dev"] 220 | marker = "python_version < \"3.10\"" 221 | dependencies = [ 222 | "typing-extensions>=3.6.4; python_version < \"3.8\"", 223 | "zipp>=0.5", 224 | ] 225 | files = [ 226 | {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"}, 227 | {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"}, 228 | ] 229 | 230 | [[package]] 231 | name = "importlib-resources" 232 | version = "6.1.1" 233 | requires_python = ">=3.8" 234 | summary = "Read resources from Python packages" 235 | groups = ["dev"] 236 | marker = "python_version < \"3.9\"" 237 | dependencies = [ 238 | "zipp>=3.1.0; python_version < \"3.10\"", 239 | ] 240 | files = [ 241 | {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, 242 | {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, 243 | ] 244 | 245 | [[package]] 246 | name = "iniconfig" 247 | version = "2.0.0" 248 | requires_python = ">=3.7" 249 | summary = "brain-dead simple config-ini parsing" 250 | groups = ["dev"] 251 | files = [ 252 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 253 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 254 | ] 255 | 256 | [[package]] 257 | name = "installer" 258 | version = "0.7.0" 259 | requires_python = ">=3.7" 260 | summary = "A library for installing Python wheels." 261 | groups = ["dev"] 262 | files = [ 263 | {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, 264 | {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, 265 | ] 266 | 267 | [[package]] 268 | name = "markdown-it-py" 269 | version = "3.0.0" 270 | requires_python = ">=3.8" 271 | summary = "Python port of markdown-it. Markdown parsing, done right!" 272 | groups = ["default", "dev"] 273 | dependencies = [ 274 | "mdurl~=0.1", 275 | ] 276 | files = [ 277 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 278 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 279 | ] 280 | 281 | [[package]] 282 | name = "mdurl" 283 | version = "0.1.2" 284 | requires_python = ">=3.7" 285 | summary = "Markdown URL utilities" 286 | groups = ["default", "dev"] 287 | files = [ 288 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 289 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 290 | ] 291 | 292 | [[package]] 293 | name = "msgpack" 294 | version = "1.0.7" 295 | requires_python = ">=3.8" 296 | summary = "MessagePack serializer" 297 | groups = ["dev"] 298 | files = [ 299 | {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, 300 | {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, 301 | {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, 302 | {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, 303 | {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, 304 | {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, 305 | {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, 306 | {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, 307 | {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, 308 | {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, 309 | {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, 310 | {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, 311 | {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, 312 | {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, 313 | {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, 314 | {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, 315 | {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, 316 | {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, 317 | {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, 318 | {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, 319 | {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, 320 | {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, 321 | {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, 322 | {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, 323 | {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, 324 | {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, 325 | {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, 326 | {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, 327 | {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, 328 | {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, 329 | {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, 330 | {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, 331 | {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, 332 | {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, 333 | {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, 334 | {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, 335 | {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, 336 | {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, 337 | {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, 338 | {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, 339 | {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, 340 | {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, 341 | {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, 342 | {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, 343 | {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, 344 | {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, 345 | {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, 346 | {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, 347 | {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, 348 | {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, 349 | {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, 350 | {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, 351 | {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, 352 | {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, 353 | {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, 354 | {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, 355 | ] 356 | 357 | [[package]] 358 | name = "packaging" 359 | version = "23.2" 360 | requires_python = ">=3.7" 361 | summary = "Core utilities for Python packages" 362 | groups = ["dev"] 363 | files = [ 364 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 365 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 366 | ] 367 | 368 | [[package]] 369 | name = "pbs-installer" 370 | version = "2024.4.24" 371 | requires_python = ">=3.8" 372 | summary = "Installer for Python Build Standalone" 373 | groups = ["dev"] 374 | files = [ 375 | {file = "pbs_installer-2024.4.24-py3-none-any.whl", hash = "sha256:f8291f0231003d279d0de8fde88fa87b7c6d7fabc2671235113cf67513ff74f5"}, 376 | {file = "pbs_installer-2024.4.24.tar.gz", hash = "sha256:19224733068b0ffa39b53afbb61544bee8ecb9503e7222ba034f07b9913e2c1c"}, 377 | ] 378 | 379 | [[package]] 380 | name = "pdm" 381 | version = "2.17.0" 382 | requires_python = ">=3.8" 383 | summary = "A modern Python package and dependency manager supporting the latest PEP standards" 384 | groups = ["dev"] 385 | dependencies = [ 386 | "blinker", 387 | "dep-logic>=0.4.0", 388 | "filelock>=3.13", 389 | "findpython<1.0.0a0,>=0.6.0", 390 | "hishel<0.1.0,>=0.0.24", 391 | "httpx[socks]<1,>0.20", 392 | "importlib-metadata>=3.6; python_version < \"3.10\"", 393 | "importlib-resources>=5; python_version < \"3.9\"", 394 | "installer<0.8,>=0.7", 395 | "msgpack>=1.0", 396 | "packaging!=22.0,>=20.9", 397 | "pbs-installer>=2024.4.18", 398 | "platformdirs", 399 | "pyproject-hooks", 400 | "python-dotenv>=0.15", 401 | "resolvelib>=1.0.1", 402 | "rich>=12.3.0", 403 | "shellingham>=1.3.2", 404 | "tomli>=1.1.0; python_version < \"3.11\"", 405 | "tomlkit<1,>=0.11.1", 406 | "truststore; python_version >= \"3.10\"", 407 | "unearth>=0.16.0", 408 | "virtualenv>=20", 409 | ] 410 | files = [ 411 | {file = "pdm-2.17.0-py3-none-any.whl", hash = "sha256:d0d4280c20537831c817bf89dd1474542534bf511d4a38b61e16ac14a3b6b6b0"}, 412 | {file = "pdm-2.17.0.tar.gz", hash = "sha256:de23086b6d0a82e40cca70c2171b8d29c19fcfc164e79b93c144025ffd0232fa"}, 413 | ] 414 | 415 | [[package]] 416 | name = "pdm" 417 | version = "2.17.0" 418 | extras = ["pytest"] 419 | requires_python = ">=3.8" 420 | summary = "A modern Python package and dependency manager supporting the latest PEP standards" 421 | groups = ["dev"] 422 | dependencies = [ 423 | "pdm==2.17.0", 424 | "pytest", 425 | "pytest-mock", 426 | ] 427 | files = [ 428 | {file = "pdm-2.17.0-py3-none-any.whl", hash = "sha256:d0d4280c20537831c817bf89dd1474542534bf511d4a38b61e16ac14a3b6b6b0"}, 429 | {file = "pdm-2.17.0.tar.gz", hash = "sha256:de23086b6d0a82e40cca70c2171b8d29c19fcfc164e79b93c144025ffd0232fa"}, 430 | ] 431 | 432 | [[package]] 433 | name = "platformdirs" 434 | version = "4.1.0" 435 | requires_python = ">=3.8" 436 | summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 437 | groups = ["dev"] 438 | files = [ 439 | {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, 440 | {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, 441 | ] 442 | 443 | [[package]] 444 | name = "pluggy" 445 | version = "1.3.0" 446 | requires_python = ">=3.8" 447 | summary = "plugin and hook calling mechanisms for python" 448 | groups = ["dev"] 449 | files = [ 450 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 451 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 452 | ] 453 | 454 | [[package]] 455 | name = "pygments" 456 | version = "2.17.2" 457 | requires_python = ">=3.7" 458 | summary = "Pygments is a syntax highlighting package written in Python." 459 | groups = ["default", "dev"] 460 | files = [ 461 | {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, 462 | {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, 463 | ] 464 | 465 | [[package]] 466 | name = "pyproject-hooks" 467 | version = "1.0.0" 468 | requires_python = ">=3.7" 469 | summary = "Wrappers to call pyproject.toml-based build backend hooks." 470 | groups = ["dev"] 471 | dependencies = [ 472 | "tomli>=1.1.0; python_version < \"3.11\"", 473 | ] 474 | files = [ 475 | {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"}, 476 | {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"}, 477 | ] 478 | 479 | [[package]] 480 | name = "pytest" 481 | version = "7.4.3" 482 | requires_python = ">=3.7" 483 | summary = "pytest: simple powerful testing with Python" 484 | groups = ["dev"] 485 | dependencies = [ 486 | "colorama; sys_platform == \"win32\"", 487 | "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", 488 | "importlib-metadata>=0.12; python_version < \"3.8\"", 489 | "iniconfig", 490 | "packaging", 491 | "pluggy<2.0,>=0.12", 492 | "tomli>=1.0.0; python_version < \"3.11\"", 493 | ] 494 | files = [ 495 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, 496 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, 497 | ] 498 | 499 | [[package]] 500 | name = "pytest-mock" 501 | version = "3.12.0" 502 | requires_python = ">=3.8" 503 | summary = "Thin-wrapper around the mock package for easier use with pytest" 504 | groups = ["dev"] 505 | dependencies = [ 506 | "pytest>=5.0", 507 | ] 508 | files = [ 509 | {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, 510 | {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, 511 | ] 512 | 513 | [[package]] 514 | name = "python-dotenv" 515 | version = "1.0.0" 516 | requires_python = ">=3.8" 517 | summary = "Read key-value pairs from a .env file and set them as environment variables" 518 | groups = ["dev"] 519 | files = [ 520 | {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, 521 | {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, 522 | ] 523 | 524 | [[package]] 525 | name = "resolvelib" 526 | version = "1.0.1" 527 | summary = "Resolve abstract dependencies into concrete ones" 528 | groups = ["dev"] 529 | files = [ 530 | {file = "resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf"}, 531 | {file = "resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309"}, 532 | ] 533 | 534 | [[package]] 535 | name = "rich" 536 | version = "13.7.1" 537 | requires_python = ">=3.7.0" 538 | summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 539 | groups = ["default", "dev"] 540 | dependencies = [ 541 | "markdown-it-py>=2.2.0", 542 | "pygments<3.0.0,>=2.13.0", 543 | "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", 544 | ] 545 | files = [ 546 | {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, 547 | {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, 548 | ] 549 | 550 | [[package]] 551 | name = "shellingham" 552 | version = "1.5.4" 553 | requires_python = ">=3.7" 554 | summary = "Tool to Detect Surrounding Shell" 555 | groups = ["dev"] 556 | files = [ 557 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 558 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 559 | ] 560 | 561 | [[package]] 562 | name = "sniffio" 563 | version = "1.3.1" 564 | requires_python = ">=3.7" 565 | summary = "Sniff out which async library your code is running under" 566 | groups = ["dev"] 567 | files = [ 568 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 569 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 570 | ] 571 | 572 | [[package]] 573 | name = "socksio" 574 | version = "1.0.0" 575 | requires_python = ">=3.6" 576 | summary = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." 577 | groups = ["dev"] 578 | files = [ 579 | {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, 580 | {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, 581 | ] 582 | 583 | [[package]] 584 | name = "tomli" 585 | version = "2.0.1" 586 | requires_python = ">=3.7" 587 | summary = "A lil' TOML parser" 588 | groups = ["dev"] 589 | marker = "python_version < \"3.11\"" 590 | files = [ 591 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 592 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 593 | ] 594 | 595 | [[package]] 596 | name = "tomlkit" 597 | version = "0.12.3" 598 | requires_python = ">=3.7" 599 | summary = "Style preserving TOML library" 600 | groups = ["dev"] 601 | files = [ 602 | {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, 603 | {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, 604 | ] 605 | 606 | [[package]] 607 | name = "truststore" 608 | version = "0.8.0" 609 | requires_python = ">= 3.10" 610 | summary = "Verify certificates using native system trust stores" 611 | groups = ["dev"] 612 | marker = "python_version >= \"3.10\"" 613 | files = [ 614 | {file = "truststore-0.8.0-py3-none-any.whl", hash = "sha256:e37a5642ae9fc48caa8f120b6283d77225d600d224965a672c9e8ef49ce4bb4c"}, 615 | {file = "truststore-0.8.0.tar.gz", hash = "sha256:dc70da89634944a579bfeec70a7a4523c53ffdb3cf52d1bb4a431fda278ddb96"}, 616 | ] 617 | 618 | [[package]] 619 | name = "typing-extensions" 620 | version = "4.9.0" 621 | requires_python = ">=3.8" 622 | summary = "Backported and Experimental Type Hints for Python 3.8+" 623 | groups = ["default", "dev"] 624 | files = [ 625 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 626 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 627 | ] 628 | 629 | [[package]] 630 | name = "unearth" 631 | version = "0.16.1" 632 | requires_python = ">=3.8" 633 | summary = "A utility to fetch and download python packages" 634 | groups = ["dev"] 635 | dependencies = [ 636 | "httpx<1,>=0.27.0", 637 | "packaging>=20", 638 | ] 639 | files = [ 640 | {file = "unearth-0.16.1-py3-none-any.whl", hash = "sha256:5a598ac1a3f185144fadc9de47f1043bff805c36118ffc40f81ef98ff22e8e37"}, 641 | {file = "unearth-0.16.1.tar.gz", hash = "sha256:988a43418fa0b78aeb628a15f6a3b02152c1787f63fe6d254c7f4e2ccf8db0a7"}, 642 | ] 643 | 644 | [[package]] 645 | name = "virtualenv" 646 | version = "20.25.0" 647 | requires_python = ">=3.7" 648 | summary = "Virtual Python Environment builder" 649 | groups = ["dev"] 650 | dependencies = [ 651 | "distlib<1,>=0.3.7", 652 | "filelock<4,>=3.12.2", 653 | "importlib-metadata>=6.6; python_version < \"3.8\"", 654 | "platformdirs<5,>=3.9.1", 655 | ] 656 | files = [ 657 | {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, 658 | {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, 659 | ] 660 | 661 | [[package]] 662 | name = "zipp" 663 | version = "3.17.0" 664 | requires_python = ">=3.8" 665 | summary = "Backport of pathlib-compatible object wrapper for zip files" 666 | groups = ["dev"] 667 | marker = "python_version < \"3.10\"" 668 | files = [ 669 | {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, 670 | {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, 671 | ] 672 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pdm-download" 3 | description = "A PDM plugin to download all packages in a lockfile for offline use." 4 | authors = [ 5 | {name = "Frost Ming", email = "me@frostming.com"}, 6 | ] 7 | dependencies = [ 8 | "rich>=13", 9 | ] 10 | requires-python = ">=3.8" 11 | readme = "README.md" 12 | license = {text = "MIT"} 13 | dynamic = ["version"] 14 | classifiers = [ 15 | "Programming Language :: Python :: 3 :: Only", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12" 21 | ] 22 | 23 | [project.urls] 24 | Repository = "https://github.com/pdm-project/pdm-download" 25 | 26 | [project.entry-points.pdm] 27 | download = "pdm_download:main" 28 | 29 | [build-system] 30 | requires = ["pdm-backend"] 31 | build-backend = "pdm.backend" 32 | 33 | [tool.pdm.version] 34 | source = "scm" 35 | 36 | [tool.pdm.dev-dependencies] 37 | dev = [ 38 | "pdm[pytest]>=2.17", 39 | ] 40 | -------------------------------------------------------------------------------- /src/pdm_download/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from pdm.core import Core 7 | 8 | 9 | def main(core: Core) -> None: 10 | from .command import Download 11 | 12 | core.register_command(Download, "download") 13 | -------------------------------------------------------------------------------- /src/pdm_download/command.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import hashlib 5 | import re 6 | from collections import defaultdict 7 | from concurrent.futures import Future, ThreadPoolExecutor 8 | from pathlib import Path 9 | from typing import TYPE_CHECKING, Any, Iterable, Iterator, Sequence, cast 10 | 11 | from pdm.cli.commands.base import BaseCommand 12 | from pdm.cli.options import lockfile_option 13 | from pdm.exceptions import PdmUsageError 14 | from rich.progress import ( 15 | BarColumn, 16 | MofNCompleteColumn, 17 | Progress, 18 | TaskProgressColumn, 19 | TextColumn, 20 | TimeRemainingColumn, 21 | ) 22 | 23 | if TYPE_CHECKING: 24 | from typing import ContextManager, TypedDict 25 | 26 | from httpx import Client, Response 27 | from pdm.models.candidates import Candidate 28 | from pdm.models.markers import EnvSpec 29 | from pdm.project import Project 30 | 31 | class FileHash(TypedDict): 32 | url: str 33 | hash: str 34 | file: str 35 | 36 | 37 | def _iter_content_compat(resp: Any, chunk_size: int) -> Iterator[bytes]: 38 | if hasattr(resp, "iter_content"): 39 | return resp.iter_content(chunk_size) 40 | return resp.iter_bytes(chunk_size) 41 | 42 | 43 | def _stream_compat( 44 | session: Client, url: str, **kwargs: Any 45 | ) -> ContextManager[Response]: 46 | if hasattr(session, "stream"): 47 | return session.stream("GET", url, **kwargs) 48 | return session.get(url, stream=True, **kwargs) 49 | 50 | 51 | def _download_package(project: Project, package: FileHash, dest: Path) -> None: 52 | from unearth import Link 53 | 54 | hash_name, hash_value = package["hash"].split(":") 55 | hasher = hashlib.new(hash_name) 56 | with project.environment.get_finder() as finder: 57 | session = finder.session 58 | with _stream_compat(session, package["url"]) as resp, dest.joinpath( 59 | package.get("file", Link(package["url"]).filename) 60 | ).open("wb") as fp: 61 | resp.raise_for_status() 62 | for chunk in _iter_content_compat(resp, chunk_size=8192): 63 | hasher.update(chunk) 64 | fp.write(chunk) 65 | if hasher.hexdigest() != hash_value: 66 | raise RuntimeError( 67 | f"Hash value of {package['file']} doesn't match. " 68 | f"Expected: {hash_value}, got: {hasher.hexdigest()}" 69 | ) 70 | 71 | 72 | def _download_packages( 73 | project: Project, packages: Sequence[FileHash], dest: Path 74 | ) -> None: 75 | if not dest.exists(): 76 | dest.mkdir(parents=True) 77 | 78 | with Progress( 79 | TextColumn("[bold success]{task.description}"), 80 | BarColumn(bar_width=None), 81 | "[progress.percentage]{task.percentage:>3.0f}%", 82 | "•", 83 | MofNCompleteColumn(), 84 | "•", 85 | TimeRemainingColumn(), 86 | "•", 87 | TaskProgressColumn(), 88 | transient=True, 89 | ) as progress: 90 | task = progress.add_task("Downloading", total=len(packages)) 91 | success_count = 0 92 | 93 | def progress_callback(future: Future) -> None: 94 | nonlocal success_count 95 | if future.exception(): 96 | project.core.ui.echo(f"[error]Error: {future.exception()}", err=True) 97 | else: 98 | success_count += 1 99 | progress.update(task, advance=1) 100 | 101 | with ThreadPoolExecutor() as pool: 102 | for package in packages: 103 | future = pool.submit(_download_package, project, package, dest) 104 | future.add_done_callback(progress_callback) 105 | 106 | project.core.ui.echo(f"[success]{success_count} packages downloaded to {dest}.") 107 | 108 | 109 | class Download(BaseCommand): 110 | """Download all packages from a lockfile for offline use""" 111 | 112 | arguments = [lockfile_option, *BaseCommand.arguments] 113 | 114 | def add_arguments(self, parser: argparse.ArgumentParser) -> None: 115 | parser.add_argument( 116 | "-d", 117 | "--dest", 118 | help="The destination directory, default to './packages'", 119 | default="./packages", 120 | type=Path, 121 | ) 122 | parser.add_argument( 123 | "--python", 124 | help="Download packages for the given Python range. E.g. '>=3.9'", 125 | ) 126 | parser.add_argument( 127 | "--platform", help="Download packages for the given platform. E.g. 'linux'" 128 | ) 129 | parser.add_argument( 130 | "--implementation", 131 | help="Download packages for the given implementation. E.g. 'cpython', 'pypy'", 132 | ) 133 | 134 | @staticmethod 135 | def _check_lock_targets(project: Project, env_spec: EnvSpec) -> None: 136 | from dep_logic.tags import EnvCompatibility 137 | from pdm.exceptions import PdmException 138 | 139 | lock_targets = project.get_locked_repository().targets 140 | ui = project.core.ui 141 | if env_spec in lock_targets: 142 | return 143 | compatibilities = [target.compare(env_spec) for target in lock_targets] 144 | if any(compat == EnvCompatibility.LOWER_OR_EQUAL for compat in compatibilities): 145 | return 146 | loose_compatible_target = next( 147 | ( 148 | target 149 | for (target, compat) in zip(lock_targets, compatibilities) 150 | if compat == EnvCompatibility.HIGHER 151 | ), 152 | None, 153 | ) 154 | if loose_compatible_target is not None: 155 | ui.warn( 156 | f"Found lock target {loose_compatible_target}, installing for env {env_spec}" 157 | ) 158 | else: 159 | errors = [ 160 | f"None of the lock targets matches the current env {env_spec}:" 161 | ] + [f" - {target}" for target in lock_targets] 162 | ui.error("\n".join(errors)) 163 | raise PdmException("No compatible lock target found") 164 | 165 | def handle(self, project: Project, options: argparse.Namespace) -> None: 166 | from itertools import chain 167 | 168 | from pdm.models.specifiers import PySpecSet 169 | 170 | env_spec = project.environment.allow_all_spec 171 | 172 | if any([options.python, options.platform, options.implementation]): 173 | replace_dict = {} 174 | if options.python: 175 | if re.match(r"[\d.]+", options.python): 176 | options.python = f">={options.python}" 177 | replace_dict["requires_python"] = PySpecSet(options.python) 178 | if options.platform: 179 | replace_dict["platform"] = options.platform 180 | if options.implementation: 181 | replace_dict["implementation"] = options.implementation 182 | env_spec = env_spec.replace(**replace_dict) 183 | 184 | if not project.lockfile.exists(): 185 | raise PdmUsageError( 186 | f"The lockfile '{options.lockfile or 'pdm.lock'}' doesn't exist." 187 | ) 188 | self._check_lock_targets(project, env_spec) 189 | locked_repository = project.get_locked_repository() 190 | all_candidates = chain.from_iterable(locked_repository.all_candidates.values()) 191 | all_candidates = [ 192 | c 193 | for c in all_candidates 194 | if c.req.marker is None or c.req.marker.matches(env_spec) 195 | ] 196 | if "static_urls" in project.lockfile.strategy: 197 | hashes = cast( 198 | "list[FileHash]", 199 | [ 200 | hash_item 201 | for candidate in all_candidates 202 | for hash_item in locked_repository.get_hashes(candidate) 203 | ], 204 | ) 205 | else: 206 | hashes = _get_file_hashes(project, all_candidates, env_spec) 207 | _download_packages(project, hashes, options.dest) 208 | 209 | 210 | def _convert_hash_option(hashes: list[FileHash]) -> dict[str, list[str]]: 211 | result: dict[str, list[str]] = defaultdict(list) 212 | for item in hashes: 213 | hash_name, hash_value = item["hash"].split(":") 214 | result[hash_name].append(hash_value) 215 | return result 216 | 217 | 218 | def _get_file_hashes( 219 | project: Project, candidates: Iterable[Candidate], env_spec: EnvSpec 220 | ) -> list[FileHash]: 221 | hashes: list[FileHash] = [] 222 | repository = project.get_repository() 223 | for candidate in candidates: 224 | can_hashes = candidate.hashes[:] 225 | if not can_hashes or not candidate.req.is_named: 226 | continue 227 | req = candidate.req.as_pinned_version(candidate.version) 228 | respect_source_order = project.environment.project.pyproject.settings.get( 229 | "resolution", {} 230 | ).get("respect-source-order", False) 231 | sources = repository.get_filtered_sources(candidate.req) 232 | comes_from = candidate.link.comes_from if candidate.link else None 233 | if req.is_named and respect_source_order and comes_from: 234 | sources = [s for s in sources if comes_from.startswith(s.url)] 235 | with project.environment.get_finder(sources, env_spec=env_spec) as finder: 236 | for package in finder.find_matches( 237 | req.as_line(), 238 | allow_yanked=True, 239 | allow_prereleases=True, 240 | hashes=_convert_hash_option(can_hashes), 241 | ): 242 | filename = package.link.filename 243 | match_hash = next( 244 | (h for h in can_hashes if h["file"] == filename), None 245 | ) 246 | if match_hash: 247 | can_hashes.remove(match_hash) 248 | hashes.append( 249 | { 250 | "url": package.link.url_without_fragment, 251 | "file": filename, 252 | "hash": match_hash["hash"], 253 | } 254 | ) 255 | 256 | for item in can_hashes: 257 | project.core.ui.echo( 258 | f"[warning]File {item['file']} not found on the repository.", 259 | err=True, 260 | ) 261 | return hashes 262 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/__init__.py -------------------------------------------------------------------------------- /tests/packages/certifi-2023.11.17-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/certifi-2023.11.17-py3-none-any.whl -------------------------------------------------------------------------------- /tests/packages/certifi-2023.11.17.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/certifi-2023.11.17.tar.gz -------------------------------------------------------------------------------- /tests/packages/chardet-3.0.4-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/chardet-3.0.4-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/packages/chardet-3.0.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/chardet-3.0.4.tar.gz -------------------------------------------------------------------------------- /tests/packages/idna-2.10-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/idna-2.10-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/packages/idna-2.10.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/idna-2.10.tar.gz -------------------------------------------------------------------------------- /tests/packages/requests-2.24.0-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/requests-2.24.0-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/packages/requests-2.24.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/requests-2.24.0.tar.gz -------------------------------------------------------------------------------- /tests/packages/urllib3-1.25.11-py2.py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/urllib3-1.25.11-py2.py3-none-any.whl -------------------------------------------------------------------------------- /tests/packages/urllib3-1.25.11.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdm-project/pdm-download/aadc2f0039216565e5fd272fcd21e0349ef4ebcc/tests/packages/urllib3-1.25.11.tar.gz -------------------------------------------------------------------------------- /tests/project/pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default"] 6 | strategy = ["cross_platform"] 7 | lock_version = "4.4" 8 | content_hash = "sha256:bcfa3f3890a1affb6aaa497bb38222f19054bc098a9ebbb8e11952226eda5048" 9 | 10 | [[package]] 11 | name = "certifi" 12 | version = "2023.11.17" 13 | requires_python = ">=3.6" 14 | summary = "Python package for providing Mozilla's CA Bundle." 15 | files = [ 16 | {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, 17 | {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, 18 | ] 19 | 20 | [[package]] 21 | name = "chardet" 22 | version = "3.0.4" 23 | summary = "Universal encoding detector for Python 2 and 3" 24 | files = [ 25 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 26 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 27 | ] 28 | 29 | [[package]] 30 | name = "idna" 31 | version = "2.10" 32 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 33 | summary = "Internationalized Domain Names in Applications (IDNA)" 34 | files = [ 35 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 36 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 37 | ] 38 | 39 | [[package]] 40 | name = "requests" 41 | version = "2.24.0" 42 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 43 | summary = "Python HTTP for Humans." 44 | dependencies = [ 45 | "certifi>=2017.4.17", 46 | "chardet<4,>=3.0.2", 47 | "idna<3,>=2.5", 48 | "urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1", 49 | ] 50 | files = [ 51 | {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, 52 | {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, 53 | ] 54 | 55 | [[package]] 56 | name = "urllib3" 57 | version = "1.25.11" 58 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 59 | summary = "HTTP library with thread-safe connection pooling, file post, and more." 60 | files = [ 61 | {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, 62 | {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, 63 | ] 64 | -------------------------------------------------------------------------------- /tests/project/pdm.static.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default"] 6 | strategy = ["cross_platform", "static_urls"] 7 | lock_version = "4.4" 8 | content_hash = "sha256:bcfa3f3890a1affb6aaa497bb38222f19054bc098a9ebbb8e11952226eda5048" 9 | 10 | [[package]] 11 | name = "certifi" 12 | version = "2023.11.17" 13 | requires_python = ">=3.6" 14 | summary = "Python package for providing Mozilla's CA Bundle." 15 | files = [ 16 | {url = "http://127.0.0.1:9876/certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, 17 | {url = "http://127.0.0.1:9876/certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, 18 | ] 19 | 20 | [[package]] 21 | name = "chardet" 22 | version = "3.0.4" 23 | summary = "Universal encoding detector for Python 2 and 3" 24 | files = [ 25 | {url = "http://127.0.0.1:9876/chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 26 | {url = "http://127.0.0.1:9876/chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 27 | ] 28 | 29 | [[package]] 30 | name = "idna" 31 | version = "2.10" 32 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 33 | summary = "Internationalized Domain Names in Applications (IDNA)" 34 | files = [ 35 | {url = "http://127.0.0.1:9876/idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 36 | {url = "http://127.0.0.1:9876/idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 37 | ] 38 | 39 | [[package]] 40 | name = "requests" 41 | version = "2.24.0" 42 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 43 | summary = "Python HTTP for Humans." 44 | dependencies = [ 45 | "certifi>=2017.4.17", 46 | "chardet<4,>=3.0.2", 47 | "idna<3,>=2.5", 48 | "urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1", 49 | ] 50 | files = [ 51 | {url = "http://127.0.0.1:9876/requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, 52 | {url = "http://127.0.0.1:9876/requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, 53 | ] 54 | 55 | [[package]] 56 | name = "urllib3" 57 | version = "1.25.11" 58 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 59 | summary = "HTTP library with thread-safe connection pooling, file post, and more." 60 | files = [ 61 | {url = "http://127.0.0.1:9876/urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, 62 | {url = "http://127.0.0.1:9876/urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, 63 | ] 64 | -------------------------------------------------------------------------------- /tests/project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | dependencies = [ 3 | "requests==2.24.0", 4 | ] 5 | requires-python = ">=3.8" 6 | 7 | [[tool.pdm.source]] 8 | url = "../packages" 9 | name = "pypi" 10 | type = "find_links" 11 | -------------------------------------------------------------------------------- /tests/test_download.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | pytest_plugins = ["pdm.pytest"] 8 | 9 | PACKAGES = Path(__file__).parent / "packages" 10 | PROJECT = Path(__file__).parent / "project" 11 | 12 | 13 | @pytest.fixture(scope="session", autouse=True) 14 | def local_file_server(): 15 | import threading 16 | from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer 17 | 18 | with ThreadingHTTPServer( 19 | ("127.0.0.1", 9876), 20 | functools.partial(SimpleHTTPRequestHandler, directory=PACKAGES), 21 | ) as httpd: 22 | thread = threading.Thread(target=httpd.serve_forever, daemon=True) 23 | thread.start() 24 | try: 25 | yield httpd 26 | finally: 27 | httpd.shutdown() 28 | thread.join() 29 | 30 | 31 | @pytest.mark.parametrize("lockfile_options", [[], ["-L", "pdm.static.lock"]]) 32 | def test_download_packages(pdm, tmp_path, lockfile_options): 33 | old_cwd = os.getcwd() 34 | os.chdir(PROJECT) 35 | try: 36 | pdm(["download", "-d", str(tmp_path)] + lockfile_options, strict=True) 37 | finally: 38 | os.chdir(old_cwd) 39 | assert set(os.listdir(tmp_path)) == set(os.listdir(PACKAGES)) 40 | --------------------------------------------------------------------------------