├── .github ├── __init__.py ├── actions.py ├── tests.py └── workflows │ ├── delete.yml │ ├── register.yml │ ├── static.yml │ ├── tests.yml │ └── update.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── index.html ├── mydependency └── index.html ├── pkg_template.html ├── private-hello └── index.html ├── public-hello └── index.html ├── static ├── index_styles.css ├── package_page.js ├── package_styles.css └── pypi_checker.js ├── transformers └── index.html └── update_pkgs.py /.github/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astariul/github-hosted-pypi/e29e6f7ea7bcc35209362f90b37619abd6b49fe0/.github/__init__.py -------------------------------------------------------------------------------- /.github/actions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import copy 3 | import re 4 | import shutil 5 | 6 | from bs4 import BeautifulSoup 7 | 8 | 9 | INDEX_FILE = "index.html" 10 | TEMPLATE_FILE = "pkg_template.html" 11 | YAML_ACTION_FILES = [".github/workflows/delete.yml", ".github/workflows/update.yml"] 12 | 13 | INDEX_CARD_HTML = ''' 14 | 15 | placeholder_name 16 | 17 | 18 | 19 | placehholder_version 20 | 21 |
22 | 23 | placeholder_description 24 | 25 |
''' 26 | 27 | 28 | def normalize(name): 29 | """ From PEP503 : https://www.python.org/dev/peps/pep-0503/ """ 30 | return re.sub(r"[-_.]+", "-", name).lower() 31 | 32 | 33 | def normalize_version(version): 34 | version = version.lower() 35 | return version[1:] if version.startswith("v") else version 36 | 37 | 38 | def is_stable(version): 39 | return not ("dev" in version or "a" in version or "b" in version or "rc" in version) 40 | 41 | 42 | def package_exists(soup, package_name): 43 | package_ref = package_name + "/" 44 | for anchor in soup.find_all('a'): 45 | if anchor['href'] == package_ref: 46 | return True 47 | return False 48 | 49 | 50 | def transform_github_url(input_url): 51 | # Split the input URL to extract relevant information 52 | parts = input_url.rstrip('/').split('/') 53 | username, repo = parts[-2], parts[-1] 54 | 55 | # Create the raw GitHub content URL 56 | raw_url = f'https://raw.githubusercontent.com/{username}/{repo}/main/README.md' 57 | return raw_url 58 | 59 | 60 | def register(pkg_name, version, author, short_desc, homepage): 61 | link = f'git+{homepage}@{version}' 62 | long_desc = transform_github_url(homepage) 63 | # Read our index first 64 | with open(INDEX_FILE) as html_file: 65 | soup = BeautifulSoup(html_file, "html.parser") 66 | norm_pkg_name = normalize(pkg_name) 67 | norm_version = normalize_version(version) 68 | 69 | if package_exists(soup, norm_pkg_name): 70 | raise ValueError(f"Package {norm_pkg_name} seems to already exists") 71 | 72 | # Create a new anchor element for our new package 73 | placeholder_card = BeautifulSoup(INDEX_CARD_HTML, 'html.parser') 74 | placeholder_card = placeholder_card.find('a') 75 | new_package = copy.copy(placeholder_card) 76 | new_package['href'] = f"{norm_pkg_name}/" 77 | new_package.contents[0].replace_with(pkg_name) 78 | spans = new_package.find_all('span') 79 | spans[1].string = norm_version # First span contain the version 80 | spans[2].string = short_desc # Second span contain the short description 81 | 82 | # Add it to our index and save it 83 | soup.find('h6', class_='text-header').insert_after(new_package) 84 | with open(INDEX_FILE, 'wb') as index: 85 | index.write(soup.prettify("utf-8")) 86 | 87 | # Then get the template, replace the content and write to the right place 88 | with open(TEMPLATE_FILE) as temp_file: 89 | template = temp_file.read() 90 | 91 | template = template.replace("_package_name", pkg_name) 92 | template = template.replace("_norm_version", norm_version) 93 | template = template.replace("_version", version) 94 | template = template.replace("_link", f"{link}#egg={norm_pkg_name}-{norm_version}") 95 | template = template.replace("_homepage", homepage) 96 | template = template.replace("_author", author) 97 | template = template.replace("_long_description", long_desc) 98 | template = template.replace("_latest_main", version) 99 | 100 | os.mkdir(norm_pkg_name) 101 | package_index = os.path.join(norm_pkg_name, INDEX_FILE) 102 | with open(package_index, "w") as f: 103 | f.write(template) 104 | 105 | 106 | def update(pkg_name, version): 107 | # Read our index first 108 | with open(INDEX_FILE) as html_file: 109 | soup = BeautifulSoup(html_file, "html.parser") 110 | norm_pkg_name = normalize(pkg_name) 111 | norm_version = normalize_version(version) 112 | 113 | if not package_exists(soup, norm_pkg_name): 114 | raise ValueError(f"Package {norm_pkg_name} seems to not exists") 115 | 116 | # Change the version in the main page (only if stable) 117 | if is_stable(version): 118 | anchor = soup.find('a', attrs={"href": f"{norm_pkg_name}/"}) 119 | spans = anchor.find_all('span') 120 | spans[1].string = norm_version 121 | with open(INDEX_FILE, 'wb') as index: 122 | index.write(soup.prettify("utf-8")) 123 | 124 | # Change the package page 125 | index_file = os.path.join(norm_pkg_name, INDEX_FILE) 126 | with open(index_file) as html_file: 127 | soup = BeautifulSoup(html_file, "html.parser") 128 | 129 | # Extract the URL from the onclick attribute 130 | button = soup.find('button', id='repoHomepage') 131 | if button: 132 | link = button.get("onclick")[len("openLinkInNewTab('"):-2] 133 | else: 134 | raise Exception("Homepage URL not found") 135 | 136 | # Create a new anchor element for our new version 137 | original_div = soup.find('section', class_='versions').findAll('div')[-1] 138 | new_div = copy.copy(original_div) 139 | anchor = new_div.find('a') 140 | new_div['onclick'] = f"load_readme('{version}', scroll_to_div=true);" 141 | new_div['id'] = version 142 | new_div['class'] = "" 143 | if not is_stable(version): 144 | new_div['class'] += "prerelease" 145 | else: 146 | # replace the latest main version 147 | main_version_span = soup.find('span', id='latest-main-version') 148 | main_version_span.string = version 149 | anchor.string = norm_version 150 | anchor['href'] = f"git+{link}@{version}#egg={norm_pkg_name}-{norm_version}" 151 | 152 | # Add it to our index 153 | original_div.insert_after(new_div) 154 | 155 | # Change the latest version (if stable) 156 | if is_stable(version): 157 | soup.html.body.div.section.find_all('span')[1].contents[0].replace_with(version) 158 | 159 | # Save it 160 | with open(index_file, 'wb') as index: 161 | index.write(soup.prettify("utf-8")) 162 | 163 | 164 | def delete(pkg_name): 165 | # Read our index first 166 | with open(INDEX_FILE) as html_file: 167 | soup = BeautifulSoup(html_file, "html.parser") 168 | norm_pkg_name = normalize(pkg_name) 169 | 170 | if not package_exists(soup, norm_pkg_name): 171 | raise ValueError(f"Package {norm_pkg_name} seems to not exists") 172 | 173 | # Remove the package directory 174 | shutil.rmtree(norm_pkg_name) 175 | 176 | # Find and remove the anchor corresponding to our package 177 | anchor = soup.find('a', attrs={"href": f"{norm_pkg_name}/"}) 178 | anchor.extract() 179 | with open(INDEX_FILE, 'wb') as index: 180 | index.write(soup.prettify("utf-8")) 181 | 182 | 183 | def main(): 184 | # Call the right method, with the right arguments 185 | action = os.environ["PKG_ACTION"] 186 | 187 | if action == "REGISTER": 188 | register( 189 | pkg_name=os.environ["PKG_NAME"], 190 | version=os.environ["PKG_VERSION"], 191 | author=os.environ["PKG_AUTHOR"], 192 | short_desc=os.environ["PKG_SHORT_DESC"], 193 | homepage=os.environ["PKG_HOMEPAGE"], 194 | ) 195 | elif action == "DELETE": 196 | delete( 197 | pkg_name=os.environ["PKG_NAME"] 198 | ) 199 | elif action == "UPDATE": 200 | update( 201 | pkg_name=os.environ["PKG_NAME"], 202 | version=os.environ["PKG_VERSION"] 203 | ) 204 | 205 | 206 | if __name__ == "__main__": 207 | main() 208 | -------------------------------------------------------------------------------- /.github/tests.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | from importlib.metadata import PackageNotFoundError, version 4 | from contextlib import contextmanager 5 | 6 | 7 | @contextmanager 8 | def run_local_pypi_index(): 9 | # WARNING : This requires the script to be run from the root of the repo 10 | p = subprocess.Popen(["python", "-m", "http.server"]) 11 | try: 12 | yield 13 | finally: 14 | p.terminate() 15 | 16 | 17 | def exists(pkg_name: str) -> bool: 18 | try: 19 | version(pkg_name) 20 | except PackageNotFoundError: 21 | return False 22 | else: 23 | return True 24 | 25 | 26 | def pip_install(pkg_name: str, upgrade: bool = False, version: str = None): 27 | package_to_install = pkg_name if version is None else f"{pkg_name}=={version}" 28 | cmd = ["python", "-m", "pip", "install", package_to_install, "--upgrade" if upgrade else "", "--extra-index-url", "http://localhost:8000"] 29 | subprocess.run([c for c in cmd if c]) 30 | 31 | 32 | def pip_uninstall(pkg_name: str): 33 | subprocess.run(["python", "-m", "pip", "uninstall", pkg_name, "-y"]) 34 | 35 | 36 | def register(pkg_name: str, pkg_version: str, pkg_link: str): 37 | env = os.environ.copy() 38 | env["PKG_ACTION"] = "REGISTER" 39 | env["PKG_NAME"] = pkg_name 40 | env["PKG_VERSION"] = pkg_version 41 | env["PKG_AUTHOR"] = "Dummy author" 42 | env["PKG_SHORT_DESC"] = "Dummy Description" 43 | env["PKG_HOMEPAGE"] = pkg_link 44 | subprocess.run(["python", ".github/actions.py"], env=env) 45 | 46 | 47 | def update(pkg_name: str, pkg_version: str): 48 | env = os.environ.copy() 49 | env["PKG_ACTION"] = "UPDATE" 50 | env["PKG_NAME"] = pkg_name 51 | env["PKG_VERSION"] = pkg_version 52 | subprocess.run(["python", ".github/actions.py"], env=env) 53 | 54 | 55 | def delete(pkg_name: str): 56 | env = os.environ.copy() 57 | env["PKG_ACTION"] = "DELETE" 58 | env["PKG_NAME"] = pkg_name 59 | subprocess.run(["python", ".github/actions.py"], env=env) 60 | 61 | 62 | def run_tests(): 63 | # This test is checking that the Github actions for registering, updating, 64 | # and deleting are working and the PyPi index is updated accordingly. 65 | # What we do is : 66 | # * Serve the HTML locally so we have a local PyPi index 67 | # * Run the actions for registering, updating, and deleting packages in 68 | # this local PyPi 69 | # * In-between, run some basic checks to see if the installation is 70 | # working properly 71 | with run_local_pypi_index(): 72 | # First, make sure the package is not installed 73 | assert not exists("public-hello") 74 | 75 | # The package `public-hello` is already registered in our local PyPi 76 | # ACTION : Let's remove it from our local PyPi 77 | delete("public-hello") 78 | 79 | # Trying to install the package, only the version uploaded to PyPi (0.0.0) 80 | # will be detected and installed (the version in our local PyPi was 81 | # successfully deleted) 82 | pip_install("public-hello") 83 | assert exists("public-hello") and version("public-hello") == "0.0.0" 84 | 85 | # ACTION : Register the package, to make it available again 86 | register("public-hello", "0.1", "https://github.com/astariul/public-hello") 87 | 88 | # Now we can install it from the local PyPi 89 | pip_install("public-hello", upgrade=True) 90 | assert exists("public-hello") and version("public-hello") == "0.1" 91 | 92 | # ACTION : Update the package with a new version 93 | update("public-hello", "0.2") 94 | 95 | # We can update the package to the newest version 96 | pip_install("public-hello", upgrade=True) 97 | assert exists("public-hello") and version("public-hello") == "0.2" 98 | 99 | # We should still be able to install the old version 100 | pip_install("public-hello", version="0.1") 101 | assert exists("public-hello") and version("public-hello") == "0.1" 102 | 103 | # Uninstall the package (for consistency with the initial state) 104 | pip_uninstall("public-hello") 105 | assert not exists("public-hello") 106 | 107 | 108 | if __name__ == "__main__": 109 | run_tests() 110 | -------------------------------------------------------------------------------- /.github/workflows/delete.yml: -------------------------------------------------------------------------------- 1 | name: delete 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | package_name: 7 | description: Package name 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | delete: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: [3.8] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Run Action 25 | env: 26 | PKG_ACTION: DELETE 27 | PKG_NAME: ${{ inputs.package_name }} 28 | run: | 29 | pip install beautifulsoup4 30 | python .github/actions.py 31 | - name: Create Pull Request 32 | uses: peter-evans/create-pull-request@v3 33 | with: 34 | commit-message: ':package: [:robot:] Delete package from PyPi index' 35 | title: '[🤖] Delete `${{ inputs.package_name }}` from PyPi index' 36 | body: Automatically generated PR, deleting `${{ inputs.package_name }}` from 37 | PyPi index. 38 | branch-suffix: random 39 | delete-branch: true 40 | -------------------------------------------------------------------------------- /.github/workflows/register.yml: -------------------------------------------------------------------------------- 1 | name: register 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | package_name: 7 | description: 'Package name' 8 | required: true 9 | type: string 10 | version: 11 | description: 'Version of the package (tag name)' 12 | required: true 13 | type: string 14 | author: 15 | description: 'Author(s) of the package' 16 | required: true 17 | type: string 18 | short_desc: 19 | description: 'A short description of the package to show on the index' 20 | required: true 21 | type: string 22 | homepage: 23 | description: 'Homepage of the package (link to the github repository)' 24 | required: true 25 | type: string 26 | 27 | jobs: 28 | update: 29 | runs-on: ubuntu-latest 30 | strategy: 31 | matrix: 32 | python-version: [3.8] 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v1 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - name: Run Action 41 | env: 42 | PKG_ACTION: REGISTER 43 | PKG_NAME: ${{ inputs.package_name }} 44 | PKG_VERSION: ${{ inputs.version }} 45 | PKG_AUTHOR: ${{ inputs.author }} 46 | PKG_SHORT_DESC: ${{ inputs.short_desc }} 47 | PKG_HOMEPAGE: ${{ inputs.homepage }} 48 | run: | 49 | pip install beautifulsoup4 50 | python .github/actions.py 51 | - name: Create Pull Request 52 | uses: peter-evans/create-pull-request@v3 53 | with: 54 | commit-message: ':package: [:robot:] Register package in PyPi index' 55 | title: '[🤖] Register `${{ inputs.package_name }}` in PyPi index' 56 | body: Automatically generated PR, registering `${{ inputs.package_name }}` in PyPi 57 | index. 58 | branch-suffix: random 59 | delete-branch: true 60 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | 45 | prod_checks: 46 | runs-on: ubuntu-latest 47 | needs: deploy 48 | steps: 49 | - uses: actions/setup-python@v5 50 | - name: Install public-hello 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install public-hello --extra-index-url https://astariul.github.io/github-hosted-pypi/ 54 | 55 | # Check if the package and its dependency was installed 56 | pip show public-hello 57 | pip show mydependency 58 | 59 | # The code from the package should be accessible 60 | python -c "from public_hello import hi; assert hi() == 'Hello world from public repository (with a dependency)'" 61 | - name: Install public-hello older version 62 | run: | 63 | python -m pip install --upgrade pip 64 | pip install public-hello==0.1 --extra-index-url https://astariul.github.io/github-hosted-pypi/ 65 | 66 | # Check if the package was installed 67 | pip show public-hello 68 | 69 | # The code from the package should be accessible 70 | python -c "from public_hello import hi; assert hi() == 'Hello world from public repository'" 71 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: # Use deploy key to ensure pushed change trigger checks as well : https://github.com/peter-evans/create-pull-request/blob/master/docs/concepts-guidelines.md#workarounds-to-trigger-further-workflow-runs 14 | ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.8" 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install beautifulsoup4 23 | - name: Run unit-tests 24 | timeout-minutes: 5 25 | run: | 26 | # Enable pipefail option, so if the tests fail, the job will fail as well 27 | set -o pipefail 28 | python .github/tests.py 29 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: update 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | package_name: 7 | description: Package name 8 | required: true 9 | type: string 10 | version: 11 | description: New version of the package (tag name) 12 | required: true 13 | type: string 14 | 15 | jobs: 16 | update: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python-version: [3.8] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Run Action 29 | env: 30 | PKG_ACTION: UPDATE 31 | PKG_NAME: ${{ inputs.package_name }} 32 | PKG_VERSION: ${{ inputs.version }} 33 | run: | 34 | pip install beautifulsoup4 35 | python .github/actions.py 36 | - name: Create Pull Request 37 | uses: peter-evans/create-pull-request@v3 38 | with: 39 | commit-message: ':package: [:robot:] Update package in PyPi index' 40 | title: '[🤖] Update `${{ inputs.package_name }}` in PyPi index' 41 | body: Automatically generated PR, updating `${{ inputs.package_name }}` in PyPi 42 | index. 43 | branch-suffix: random 44 | delete-branch: true 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nicolas Remond 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 |

github-hosted-pypi

2 | 3 |

4 | Make all your private packages accessible in one place
with this github-hosted PyPi index 5 |

6 | 7 | --- 8 | 9 |

10 | Description • 11 | Try it ! • 12 | Get Started • 13 | Modify indexed packages • 14 | FAQ • 15 | A word about supply chain attacks • 16 | Contribute • 17 | References 18 |

19 | 20 | --- 21 | 22 | ## Features 23 | 24 | * **:octocat: Github-hosted** 25 | * **🚀 Template ready to deploy** 26 | * **🔆 Easy to use** through Github Actions 27 | * **🚨 Secure** : Warns you if your package is vulnerable to supply chain attacks 28 | 29 | ## Description 30 | 31 | This repository is a Github page used as a PyPi index, conform to [PEP503](https://www.python.org/dev/peps/pep-0503/). 32 | 33 | You can use it to group all your packages in one place, and access it easily through `pip`, almost like any other package publicly available ! 34 | 35 | --- 36 | 37 | _While the PyPi index is public, private packages indexed here are kept private : you will need Github authentication to be able to retrieve it._ 38 | 39 | ## Try it ! 40 | 41 | Visit [astariul.github.io/github-hosted-pypi/](http://astariul.github.io/github-hosted-pypi/) and try to install packages indexed there ! 42 | 43 | --- 44 | 45 | Try to install the package `public-hello` : 46 | ```console 47 | pip install public-hello --extra-index-url https://astariul.github.io/github-hosted-pypi/ 48 | ``` 49 | 50 | It will also install the package `mydependency`, automatically ! 51 | 52 | Try it with : 53 | 54 | ```python 55 | from public_hello import hi 56 | print(hi()) 57 | ``` 58 | 59 | You can also install a specific version : 60 | 61 | ```console 62 | pip install public-hello==0.1 --extra-index-url https://astariul.github.io/github-hosted-pypi/ 63 | ``` 64 | 65 | --- 66 | 67 | Now try to install the package `private-hello` : 68 | ```console 69 | pip install private-hello --extra-index-url https://astariul.github.io/github-hosted-pypi/ 70 | ``` 71 | 72 | _It will not work, because it's private and only me can access it !_ 73 | 74 | ## Get started 75 | 76 | * Use this template and create your own repository : 77 | 78 |

79 | Use template 80 |

81 | 82 | * Go to `Settings` of your repository, and enable Github Page 83 | * Customize `index.html` and `pkg_template.html` to your liking 84 | * You're ready to go ! Visit `.github.io/` to see your PyPi index 85 | 86 | ## Modify indexed packages 87 | 88 | Now that your PyPi index is setup, you can register / update / delete packages indexed. 89 | _Github actions are setup to do it automatically for you._ 90 | 91 | You just have to : 92 | * Go to the `Actions` tab of your repository 93 | * Click the right workflow (`register` / `update` / `delete`) and trigger it manually 94 | * Fill the form and start the workflow 95 | * Wait a bit 96 | * Check the new PR opened (ensure the code added correspond to what you want) 97 | * Merge the PR 98 | 99 | ## FAQ 100 | 101 | #### Q. Is it secure ? 102 | 103 | As you may know, `pip` can install Github-hosted package if given in the form `pip install git+`. This PyPi index is just an index of links to other Github repository. 104 | 105 | Github pages are public, so this PyPi index is public. But it just contain links to other Github repositories, no code is hosted on this PyPi index ! 106 | 107 | If the repository hosting code is private, you will need to authenticate with Github to be able to clone it, effectively making it private. 108 | 109 | --- 110 | 111 | If you wonder more specifically about supply chain attacks, check [the section about it](#a-word-about-supply-chain-attacks) ! 112 | 113 | #### Q. What happen behind the scenes ? 114 | 115 | When running `pip install --extra-index-url https://astariul.github.io/github-hosted-pypi/`, the following happen : 116 | 117 | 1. `pip` will look at `https://pypi.org/`, the default, public index, trying to find a package with the specified name. 118 | 2. If it can't find, it will look at `https://astariul.github.io/github-hosted-pypi/`. 119 | 3. If the package is found there, the link of the package is returned to `pip` (`git+@`). 120 | 4. From this link, `pip` understand it's a Github repository and will clone the repository (at the specific tag) locally. 121 | 5. From the cloned repository, `pip` install the package. 122 | 6. `pip` install any missing dependency with the same steps. 123 | 124 | _Authentication happen at step 4, when cloning the repository._ 125 | 126 | #### Q. What are the best practices for using this PyPi index ? 127 | 128 | The single best practice is using Github releases. This allow your package to have a version referred by a specific tag. 129 | To do this : 130 | 131 | * Push your code in a repository. 132 | * Create a new Github release. Ensure you follow [semantic versioning](https://semver.org/). It will create a tag. 133 | * Ensure you can install the package with `pip install git+@` 134 | * When putting the package on this index, put the full link (`git+@`). 135 | 136 | #### Q. What if the name of my package is already taken by a package in the public index ? 137 | 138 | You can just specify a different name for your indexed package. Just give it a different name in the form when registering it. 139 | 140 | For example if you have a private package named `tensorflow`, when you register it in this index, you can name it `my_cool_tensorflow`, so there is no name-collision with the public package `tensorflow`. 141 | Then you can install it with `pip install my_cool_tensorflow --extra-index-url https://astariul.github.io/github-hosted-pypi/`. 142 | 143 | Then from `python`, you can just do : 144 | ```python 145 | import tensorflow 146 | ``` 147 | 148 | --- 149 | 150 | **But be careful about this !** While it's possible to handle it like this, it's always better to have a unique name for your package, to avoid confusion but also for [security](#a-word-about-supply-chain-attacks) ! 151 | 152 | #### Q. How to download private package from Docker ? 153 | 154 | Building a Docker image is not interactive, so there is no prompt to type username and password. 155 | Instead, you should put your Github credentials in a `.netrc` file, so `pip` can authenticate when cloning from Github. 156 | To do this securely on Docker, you should use Docker secrets. Here is a quick tutorial on how to do : 157 | 158 | **Step 1** : Save your credentials in a secret file. Follow this example : 159 | 160 | ``` 161 | machine github.com 162 | login 163 | password 164 | ``` 165 | 166 | ⚠️ _Syntax is important : ensure you're using **tabulation**, and the line endings are **`\n`**. 167 | So careful if you're using a IDE that replace tabs by spaces or if you're on Windows (where line endings are `\r\n`) !_ 168 | 169 | Let's name this file `gh_auth.txt`. 170 | 171 | **Step 2** : Create your Docker file. In the docker file you should mount the secret file in `.netrc`, and run the command where you need authentication. For example : 172 | 173 | ```dockerfile 174 | # syntax=docker/dockerfile:experimental 175 | FROM python:3 176 | 177 | RUN --mount=type=secret,id=gh_auth,dst=/root/.netrc pip install --extra-index-url https://astariul.github.io/github-hosted-pypi/ 178 | ``` 179 | 180 | **Step 3** : Build your Docker image, specifying the location of the secret created in step 1 : 181 | 182 | `sudo DOCKER_BUILDKIT=1 docker build --secret id=gh_auth,src=./gh_auth.txt .` 183 | 184 | --- 185 | 186 | **_If you have any questions or ideas to improve this FAQ, please open a PR / blank issue !_** 187 | 188 | ## A word about supply chain attacks 189 | 190 | As you saw earlier, this github-hosted PyPi index rely on the `pip` feature `--extra-index-url`. Because of how this feature works, it is vulnerable to supply chain attacks. 191 | 192 | For example, let's say you have a package named `fbi_package` version `2.8.3` hosted on your private PyPi index. 193 | 194 | An attacker could create a malicious package with the same name and a higher version (for example `99.0.0`). 195 | When you run `pip install fbi_package --extra-index-url my_pypi_index.com`, under the hood `pip` will download the latest version of the package, which is the malicious package ! 196 | 197 | --- 198 | 199 | While this repository makes it very convenient to have your own PyPi index, be aware that the page is public, therefore anyone can see which package name you're using and create a malicious package with this same name... 200 | 201 | That's why we included automated checks into this private PyPi index. Whenever you access the page of your package, PyPi API is called, and if a package with the same name and a higher version is found, the install command is replaced with a warning. 202 | 203 | You can see a demo of such warning at [https://astariul.github.io/github-hosted-pypi/transformers/](https://astariul.github.io/github-hosted-pypi/transformers/). 204 | 205 | If you see this warning, don't install the package ! Instead, change the name of your package or upgrade the version above its public counterpart. 206 | 207 | Be careful out there ! 208 | 209 | ## Contribute 210 | 211 | Issues and PR are welcome ! 212 | 213 | If you come across anything weird / that can be improved, please get in touch ! 214 | 215 | ## References 216 | 217 | **This is greatly inspired from [this repository](https://github.com/ceddlyburge/python-package-server).** 218 | It's just a glorified version, with cleaner pages and github actions for easily adding, updating and removing packages from your index. 219 | 220 | Also check the [blogpost](https://www.freecodecamp.org/news/how-to-use-github-as-a-pypi-server-1c3b0d07db2/) of the original author ! 221 | 222 | --- 223 | 224 | _Icon used in the page was made by [Freepik](https://www.flaticon.com/authors/freepik) from [Flaticon](https://www.flaticon.com/)_ 225 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | Pypi - YourCompany 23 | 24 | 25 | 26 | 95 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /mydependency/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Pypi - YourCompany 32 | 33 | 34 | 35 | 36 |
37 |
38 | 44 | mydependency 45 | 46 | 47 | 48 | v1.0 49 | 50 | 53 |
54 | 55 | 60 | 61 |
 62 |       pip install mydependency --extra-index-url https://astariul.github.io/github-hosted-pypi/
 63 |     
64 | 65 |
66 | 67 |
68 |
69 | 70 | Project links 71 | 72 | 75 |

76 | 77 | Author : 78 | 79 | Nicolas Remond 80 |

81 |
82 |
83 | 84 | 1.0 85 | 86 |
87 |
88 |
89 | 90 |
91 |
92 | Description 93 |
94 |

95 |

96 |
97 |
98 |
99 | 100 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /pkg_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Pypi - YourCompany 32 | 33 | 34 | 35 | 36 |
37 |
38 | 44 | _package_name 45 | 46 | 47 | 48 | _version 49 | 50 | 53 |
54 | 55 | 60 | 61 |
 62 |       pip install _package_name --extra-index-url https://astariul.github.io/github-hosted-pypi/
 63 |     
64 | 65 |
66 | 67 |
68 |
69 | 70 | Project links 71 | 72 | 75 |

76 | 77 | Author : 78 | 79 | _author 80 |

81 |
82 | 87 |
88 |
89 | 90 |
91 |
92 | Description 93 |
94 |

95 |

96 |
97 |
98 |
99 | 100 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /private-hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Pypi - YourCompany 32 | 33 | 34 | 35 | 36 |
37 |
38 | 44 | private-hello 45 | 46 | 47 | 48 | v0.4.5 49 | 50 | 53 |
54 | 55 | 60 | 61 |
 62 |       pip install private-hello --extra-index-url https://astariul.github.io/github-hosted-pypi/
 63 |     
64 | 65 |
66 | 67 |
68 |
69 | 70 | Project links 71 | 72 | 75 |

76 | 77 | Author : 78 | 79 | Nicolas Remond 80 |

81 |
82 | 87 |
88 |
89 | 90 |
91 |
92 | Description 93 |
94 |

95 |

96 |
97 |
98 |
99 | 100 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /public-hello/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 27 | Pypi - YourCompany 28 | 29 | 30 | 31 |
32 |
33 | 41 | public-hello 42 | 43 | 44 | 45 | 0.2 46 | 47 | 50 |
51 | 56 |
 57 |       pip install public-hello --extra-index-url https://astariul.github.io/github-hosted-pypi/
 58 |     
59 |
60 |
61 |
62 | 63 | Project links 64 | 65 | 68 |

69 | 70 | Author : 71 | 72 | Nicolas Remond 73 |

74 |
75 |
76 | 77 | 0.1 78 | 79 |
80 |
81 | 82 | 0.2 83 | 84 |
85 | 90 |
91 |
92 |
93 |
94 | Description 95 |
96 |

97 |

98 |
99 |
100 |
101 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /static/index_styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Montserrat"; 3 | padding-bottom: 64px; 4 | } 5 | 6 | code { 7 | font-size: 100%; 8 | display: inline-block !important; 9 | font-size: 1.3rem; 10 | } 11 | 12 | .header { 13 | margin-top: 6rem; 14 | text-align: center; 15 | } 16 | 17 | .text-header { 18 | text-transform: uppercase; 19 | font-size: 1.4rem; 20 | letter-spacing: .2rem; 21 | font-weight: 600; 22 | } 23 | 24 | .card { 25 | display: inline-block; 26 | height: auto; 27 | min-width: 32%; 28 | padding: 0.5rem 2rem; 29 | margin: 0.5rem 0.5rem; 30 | color: #555; 31 | font-size: 1.5rem; 32 | font-weight: 600; 33 | line-height: 1.5; 34 | letter-spacing: .05rem; 35 | text-decoration: none; 36 | white-space: normal; 37 | background-color: transparent; 38 | border-radius: 4px; 39 | border: 1px solid #bbb; 40 | cursor: pointer; 41 | box-sizing: border-box; 42 | } 43 | 44 | .card:hover { 45 | border-color: darkcyan; 46 | color: darkcyan; 47 | } 48 | 49 | .version { 50 | font-size: 1rem; 51 | font-style: italic; 52 | } 53 | 54 | .description { 55 | font-weight: 300; 56 | } 57 | 58 | .redalert { 59 | border-color: crimson; 60 | color: crimson; 61 | } 62 | 63 | .redalert:hover { 64 | border-color: tomato; 65 | color: tomato; 66 | } 67 | 68 | /* Footer */ 69 | body { 70 | margin: 0; 71 | display: flex; 72 | flex-direction: column; 73 | min-height: 100vh; 74 | } 75 | 76 | .content { 77 | flex: 1; 78 | padding: 20px; 79 | } 80 | 81 | .footer { 82 | background-color: #f5f5f5; 83 | color: #08183f; 84 | text-align: center; 85 | padding: 20px; 86 | position: fixed; 87 | bottom: 0; 88 | width: 100%; 89 | } 90 | -------------------------------------------------------------------------------- /static/package_page.js: -------------------------------------------------------------------------------- 1 | function openLinkInNewTab(link) { 2 | window.open(link, '_blank'); 3 | } 4 | 5 | function load_readme(version, scroll_to_div=false){ 6 | addDynamicClickDelegation(`${version}`); 7 | 8 | let urlVersion = url_readme_main.replace('main', version); 9 | fetch(urlVersion) 10 | .then(response => { 11 | if (!response.ok) { 12 | if (response.status == 404) { 13 | return 'No README found for this version'; 14 | } 15 | throw new Error(`Failed to fetch content. Status code: ${response.status}`); 16 | } 17 | return response.text(); 18 | }) 19 | .then(markupContent => { 20 | const contentDivs = document.querySelectorAll('.versions div'); 21 | contentDivs.forEach(div => div.classList.remove('selected')); 22 | 23 | document.getElementById(version).classList.add('selected'); 24 | document.getElementById('markdown-container').innerHTML = marked.parse(markupContent); 25 | if (scroll_to_div) { 26 | // document.getElementById('description_pkg').scrollIntoView(); 27 | history.replaceState(null, null, '#'+version); 28 | } 29 | }) 30 | .catch(error => { 31 | console.error('Error:', error.message); 32 | }); 33 | } 34 | 35 | function put_readme(version, markupContent, scroll_to_div=false){ 36 | addDynamicClickDelegation(`${version}`); 37 | 38 | const contentDivs = document.querySelectorAll('.versions div'); 39 | contentDivs.forEach(div => div.classList.remove('selected')); 40 | 41 | document.getElementById(version).classList.add('selected'); 42 | document.getElementById('markdown-container').innerHTML = marked.parse(markupContent); 43 | if (scroll_to_div) { 44 | // document.getElementById('description_pkg').scrollIntoView(); 45 | history.replaceState(null, null, '#'+version); 46 | } 47 | } 48 | 49 | function warn_unsafe() { 50 | document.getElementById('installdanger').hidden = false; 51 | document.getElementById('installcmd').hidden = true; 52 | } 53 | 54 | function redirectToIndex() { 55 | window.location.href = "../index.html"; 56 | } 57 | 58 | function addDynamicClickDelegation(parentId) { 59 | const parentDiv = document.getElementById(parentId); 60 | 61 | if (parentDiv) { 62 | parentDiv.addEventListener('click', function (event) { 63 | if (event.target !== this) { 64 | event.stopPropagation(); 65 | this.click(); // Trigger the parent div's onclick function 66 | } 67 | }); 68 | } 69 | } 70 | 71 | function removeHrefFromAnchors() { 72 | var versionsSection = document.getElementById('versions'); 73 | if (versionsSection) { 74 | var anchors = versionsSection.getElementsByTagName('a'); 75 | for (var i = 0; i < anchors.length; i++) { 76 | anchors[i].removeAttribute('href'); 77 | } 78 | } 79 | } 80 | 81 | window.onload = function() { 82 | removeHrefFromAnchors(); 83 | }; 84 | -------------------------------------------------------------------------------- /static/package_styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Montserrat"; 3 | padding-bottom: 64px; 4 | } 5 | 6 | code { 7 | font-size: 100%; 8 | display: inline-block !important; 9 | font-size: 1.4rem; 10 | } 11 | 12 | .header { 13 | display: flex; 14 | flex-direction:row; 15 | justify-content: flex-start; 16 | gap: 1rem; 17 | margin-top: 15rem; 18 | font-size: 3.6rem; 19 | font-weight: 400; 20 | letter-spacing: -.1rem; 21 | } 22 | 23 | .text-header { 24 | text-transform: uppercase; 25 | font-size: 1.4rem; 26 | letter-spacing: .2rem; 27 | font-weight: 600; 28 | } 29 | 30 | #repoHomepage { 31 | margin-top: 1rem; 32 | } 33 | 34 | .version { 35 | font-size: 2.3rem; 36 | font-style: italic; 37 | font-weight: 300; 38 | margin-top: auto; 39 | margin-bottom: auto; 40 | } 41 | 42 | .elem { 43 | margin: 1rem 0rem; 44 | } 45 | 46 | .danger-button { 47 | text-transform: none; 48 | border-color: crimson; 49 | font-family: "Montserrat"; 50 | color: crimson; 51 | pointer-events: none; 52 | font-size: 1.2rem; 53 | margin-bottom: .2rem; 54 | } 55 | 56 | button, button:focus { 57 | border-color: #1EAEDB; 58 | color: #1EAEDB; 59 | } 60 | 61 | button:hover { 62 | border-color: white; 63 | color: white; 64 | background-color: #1EAEDB; 65 | } 66 | 67 | .versions { 68 | display: flex; 69 | flex-direction: column-reverse; 70 | } 71 | 72 | .versions div { 73 | display: block; 74 | text-decoration: none; 75 | padding: 10px 20px; 76 | cursor: pointer; 77 | } 78 | 79 | .versions div a:nth-of-type(2) { 80 | display: none; 81 | } 82 | 83 | .readme-block { 84 | font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji; 85 | } 86 | 87 | .selected { 88 | background-color: #1EAEDB; 89 | border-radius: 5rem; 90 | } 91 | 92 | .selected a { 93 | color: white; 94 | font-weight: bold; 95 | } 96 | 97 | .selected.prerelease { 98 | background: #ffdf76; /* Background for selected and prerelease elements */ 99 | position: relative; /* Position relative for absolute positioning of pseudo-element */ 100 | } 101 | 102 | .selected.prerelease a{ 103 | color: #664e04; 104 | } 105 | 106 | .selected.prerelease::after { 107 | content: "Prerelease"; /* Text to display */ 108 | position: absolute; 109 | right: 15px; /* Adjust as needed */ 110 | color: white; 111 | font-size: 12px; 112 | padding: 2px 5px; 113 | background-color: #555555; 114 | border-radius: 3px; 115 | } 116 | 117 | .main { 118 | position: relative; 119 | } 120 | 121 | .main::after { 122 | content: "Latest stable"; /* Text to display */ 123 | position: absolute; 124 | right: 15px; /* Adjust as needed */ 125 | color: white; 126 | font-size: 12px; 127 | padding: 2px 5px; 128 | background-color: #555555; 129 | border-radius: 3px; 130 | } 131 | 132 | /* Footer */ 133 | body { 134 | margin: 0; 135 | display: flex; 136 | flex-direction: column; 137 | min-height: 100vh; 138 | } 139 | 140 | .content { 141 | flex: 1; 142 | padding: 20px; 143 | } 144 | 145 | .footer { 146 | background-color: #f5f5f5; 147 | color: #08183f; 148 | text-align: center; 149 | padding: 20px; 150 | position: fixed; 151 | bottom: 0; 152 | width: 100%; 153 | } 154 | 155 | .goback-button { 156 | display: inline-flex; 157 | align-items: center; 158 | justify-content: center; 159 | text-decoration: none; 160 | border-radius: 50%; 161 | overflow: hidden; 162 | width: fit-content; 163 | height: fit-content; 164 | background-color: #1EAEDB; 165 | padding-left: 0%; 166 | padding-right: 0%; 167 | margin-bottom: 0%; 168 | } 169 | 170 | .goback-butto svg path { 171 | fill: #1EAEDB; 172 | } 173 | -------------------------------------------------------------------------------- /static/pypi_checker.js: -------------------------------------------------------------------------------- 1 | function semverCompare(a, b) { 2 | // Remove leading letters, such as `v` (`v4.23` becomes `4.23`) 3 | const clean = (v) => v.replace(/^[a-zA-Z]+/, ""); 4 | a = clean(a); 5 | b = clean(b); 6 | 7 | // Actual comparison 8 | if (a.startsWith(b + "-")) return -1 9 | if (b.startsWith(a + "-")) return 1 10 | return a.localeCompare(b, undefined, { numeric: true, sensitivity: "case", caseFirst: "upper" }) 11 | } 12 | 13 | function check_supply_chain_attack(package_name, package_version, callback_if_unsafe) { 14 | $.ajax({ 15 | type: 'GET', 16 | crossDomain: true, 17 | dataType: 'json', 18 | url: `https://pypi.org/pypi/${package_name}/json`, 19 | error: function(response) { 20 | console.log(`Couldn't find ${package_name} on PyPi : you are safe from supply chain attacks.`); 21 | }, 22 | success: function(jsondata){ 23 | // If the package exists, ensure our package has a higher version 24 | // And if it doesn't have a higher version, call the callback so that 25 | // we can take action 26 | var pypi_vers = jsondata['info']['version']; 27 | 28 | if (semverCompare(package_version, pypi_vers) <= 0) { 29 | callback_if_unsafe(); 30 | } else { 31 | console.log(`${package_name} exists on PyPi, but the version is lower (${package_version} > ${pypi_vers}), so you are safe from supply chain attacks.`) 32 | } 33 | } 34 | }) 35 | } -------------------------------------------------------------------------------- /transformers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Pypi - YourCompany 32 | 33 | 34 | 35 | 36 |
37 |
38 | 44 | transformers 45 | 46 | 47 | 48 | v3.0.0 49 | 50 | 53 |
54 | 55 | 60 | 61 |
 62 |       pip install transformers --extra-index-url https://astariul.github.io/github-hosted-pypi/
 63 |     
64 | 65 |
66 | 67 |
68 |
69 | 70 | Project links 71 | 72 | 75 |

76 | 77 | Author : 78 | 79 | Nicolas Remond 80 |

81 |
82 | 87 |
88 |
89 | 90 |
91 |
92 | Description 93 |
94 |

95 |

96 |
97 |
98 |
99 | 100 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /update_pkgs.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | sys.path.append(os.path.join(os.path.dirname(__file__), ".github")) 5 | from actions import main as github_action 6 | 7 | def execute_main(pkg_name, versions, short_desc, homepage): 8 | # Delete 9 | os.environ["PKG_ACTION"] = "DELETE" 10 | os.environ["PKG_NAME"] = pkg_name 11 | github_action() 12 | print(f"Package {pkg_name} deleted") 13 | 14 | # Register 15 | os.environ["PKG_ACTION"] = "REGISTER" 16 | os.environ["PKG_NAME"] = pkg_name 17 | os.environ["PKG_VERSION"] = versions[0] 18 | os.environ["PKG_AUTHOR"] = "Nicolas Remond" 19 | os.environ["PKG_SHORT_DESC"] = short_desc 20 | os.environ["PKG_HOMEPAGE"] = homepage 21 | github_action() 22 | print(f"Package {pkg_name} registered") 23 | 24 | # Update 25 | for version in versions[1:]: 26 | os.environ["PKG_ACTION"] = "UPDATE" 27 | os.environ["PKG_NAME"] = pkg_name 28 | os.environ["PKG_VERSION"] = version 29 | github_action() 30 | print(f"Package {pkg_name} updated to version {version}") 31 | print(f"Package {pkg_name} done") 32 | 33 | 34 | 35 | if __name__ == "__main__": 36 | # transformers 37 | pkg_name = "transformers" 38 | versions = ["v3.0.0"] 39 | short_desc = 'A simulator for electrical components' 40 | homepage = 'https://github.com/huggingface/transformers' 41 | execute_main(pkg_name, versions, short_desc, homepage) 42 | 43 | # public-hello 44 | pkg_name = "public-hello" 45 | versions = ["0.1", "0.2", "0.3.dev0"] 46 | short_desc = 'A public github-hosted repo, with a dependency to another package.' 47 | homepage = 'https://github.com/astariul/public-hello' 48 | execute_main(pkg_name, versions, short_desc, homepage) 49 | 50 | 51 | # mydependency 52 | pkg_name = "mydependency" 53 | versions = ["v1.0"] 54 | short_desc = 'A public github-hosted repo.' 55 | homepage = 'https://github.com/astariul/mydependency' 56 | execute_main(pkg_name, versions, short_desc, homepage) 57 | 58 | # private-hello 59 | pkg_name = "private-hello" 60 | versions = ["v0.4.5"] 61 | short_desc = 'This is an example of a private, github-hosted package. Only me can access this repo, you can try to install it with the pip command but a password is required : only people with repo access can download it.' 62 | homepage = 'https://github.com/astariul/private-hello' 63 | execute_main(pkg_name, versions, short_desc, homepage) 64 | 65 | 66 | --------------------------------------------------------------------------------