├── .cc-metadata.yml ├── .flake8 ├── .github ├── CODEOWNERS └── workflows │ ├── enable_workflows.yml │ ├── manage_issues.yml │ ├── normalize_repos.yml │ ├── push_data_to_ccos.yml │ └── sync_community_teams.yml ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── _ARCHIVE └── remove_hacktoberfest.py ├── ccos ├── data │ ├── asana.py │ ├── get_community_team_data.py │ ├── get_repo_data.py │ └── push_data_via_git.py ├── gh_utils.py ├── log.py ├── manage │ └── projects.yml ├── norm │ ├── branch_protections.yml │ ├── get_labels.py │ ├── labels.yml │ ├── models.py │ ├── set_labels.py │ ├── skills.yml │ └── validate_issues.py ├── schema.docs.graphql └── teams │ ├── get_community_team_data.py │ ├── set_codeowners.py │ └── set_teams_on_github.py ├── dev ├── test.sh └── tools.sh ├── enable_workflows.py ├── manage_new_issues_and_pull_requests.py ├── normalize_repos.py ├── push_data_to_ccos.py ├── pyproject.toml ├── sync_community_teams.py └── wip_sync_community_skills └── get_community_skills.py /.cc-metadata.yml: -------------------------------------------------------------------------------- 1 | # Whether this GitHub repo is for a CC-led engineering project 2 | engineering_project: true 3 | # Name of the repository/project in English 4 | english_name: CC Open Source scripts 5 | # All technologies used 6 | technologies: Python 7 | # Whether this repository should be featured on the CC Open Source site's "Projects" page 8 | featured: false 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 79 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/about-code-owners 2 | # If you want to match two or more code owners with the same pattern, all the 3 | # code owners must be on the same line. If the code owners are not on the same 4 | # line, the pattern matches only the last mentioned code owner. 5 | * @creativecommons/technology 6 | -------------------------------------------------------------------------------- /.github/workflows/enable_workflows.yml: -------------------------------------------------------------------------------- 1 | name: Enable workflows 2 | on: 3 | schedule: 4 | - cron: '22 22 1 * *' # Monthly on the 1st at 22:22 5 | workflow_dispatch: 6 | 7 | jobs: 8 | normalize_repos: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | # https://github.com/actions/setup-python 13 | - name: Setup Python 3.11 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.11' 17 | 18 | - name: Install system dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python -m pip install pipenv 22 | 23 | # https://github.com/actions/checkout 24 | - uses: actions/checkout@v4 25 | with: 26 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 27 | 28 | - name: Install app dependencies 29 | run: pipenv sync --system 30 | 31 | - name: Run script with token in env 32 | run: ./enable_workflows.py \ 33 | ccos-scripts 34 | env: 35 | ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/manage_issues.yml: -------------------------------------------------------------------------------- 1 | name: Manage issues and pull requests in projects 2 | on: 3 | schedule: 4 | - cron: "45 * * * *" # Hourly at 45 minutes past the hour (**:45) 5 | workflow_dispatch: 6 | 7 | jobs: 8 | 9 | manage_issues_and_pull_requests: 10 | name: Manage issues and pull requests 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | # https://github.com/actions/setup-python 15 | - name: Install Python 3.11 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.11' 19 | 20 | - name: Install pipenv 21 | run: | 22 | pip install --upgrade pip 23 | pip install pipenv 24 | 25 | # https://github.com/actions/checkout 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Install Python dependencies 30 | run: pipenv sync --system 31 | 32 | - name: run script to track open and untracked issues and pull requests 33 | run: ./manage_new_issues_and_pull_requests.py 34 | env: 35 | ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/normalize_repos.yml: -------------------------------------------------------------------------------- 1 | name: Normalize Repos 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' # Daily at midnight (00:00) 5 | workflow_dispatch: 6 | 7 | jobs: 8 | normalize_repos: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | # https://github.com/actions/setup-python 13 | - name: Setup Python 3.11 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.11' 17 | 18 | - name: Install system dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python -m pip install pipenv 22 | 23 | # https://github.com/actions/checkout 24 | - uses: actions/checkout@v4 25 | with: 26 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }} 27 | 28 | - name: Install app dependencies 29 | run: pipenv sync --system 30 | 31 | - name: Run script with token in env 32 | run: ./normalize_repos.py 33 | env: 34 | ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 35 | 36 | # https://github.com/actions/upload-artifact 37 | - name: Export a report of invalid issues 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: invalid-issue-report 41 | path: /tmp/invalid_issues.yml 42 | -------------------------------------------------------------------------------- /.github/workflows/push_data_to_ccos.yml: -------------------------------------------------------------------------------- 1 | name: Push data to CC Open Source 2 | on: 3 | schedule: 4 | - cron: '15 0 * * *' # Daily at midnight:15 (00:15) 5 | workflow_dispatch: 6 | 7 | jobs: 8 | push_data: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | # https://github.com/actions/setup-python 13 | - name: Setup Python 3.11 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.11' 17 | 18 | - name: Install system dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python -m pip install pipenv 22 | 23 | # https://github.com/actions/checkout 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install app dependencies 28 | run: pipenv sync --system 29 | 30 | - name: Run script with tokens in env 31 | run: ./push_data_to_ccos.py 32 | env: 33 | ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 34 | ADMIN_ASANA_TOKEN: ${{ secrets.ADMIN_ASANA_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/sync_community_teams.yml: -------------------------------------------------------------------------------- 1 | name: Sync Community Teams with GitHub 2 | on: 3 | schedule: 4 | - cron: '30 0 * * *' # Daily at midnight:30 (00:30) 5 | workflow_dispatch: 6 | 7 | jobs: 8 | sync_community_teams: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | # https://github.com/actions/setup-python 13 | - name: Setup Python 3.11 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.11' 17 | 18 | - name: Install system dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python -m pip install pipenv 22 | 23 | # https://github.com/actions/checkout 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install app dependencies 28 | run: pipenv sync --system 29 | 30 | - name: Run script with tokens in env 31 | run: ./sync_community_teams.py 32 | env: 33 | ADMIN_GITHUB_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN }} 34 | ADMIN_ASANA_TOKEN: ${{ secrets.ADMIN_ASANA_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## STANDARD PYTHON GITIGNORE ## 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | 134 | ## STANDARD MACOS GITIGNORE ## 135 | 136 | # General 137 | .DS_Store 138 | .AppleDouble 139 | .LSOverride 140 | 141 | # Icon must end with two \r 142 | Icon 143 | 144 | # Thumbnails 145 | ._* 146 | 147 | # Files that might appear in the root of a volume 148 | .DocumentRevisions-V100 149 | .fseventsd 150 | .Spotlight-V100 151 | .TemporaryItems 152 | .Trashes 153 | .VolumeIcon.icns 154 | .com.apple.timemachine.donotpresent 155 | 156 | # Directories potentially created on remote AFP share 157 | .AppleDB 158 | .AppleDesktop 159 | Network Trash Folder 160 | Temporary Items 161 | .apdisk 162 | 163 | # Editor/IDE configs 164 | .idea/ 165 | .vscode/ 166 | 167 | search_roadmap_export/config.py 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Creative Commons 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 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | black = ">=24.3.0" 8 | flake8 = "*" 9 | isort = "*" 10 | 11 | [packages] 12 | asana = ">=3.2,<4" # "Please use v3.2.X for stable / production environments" 13 | cryptography = ">=42.0.4" # dependency, pinned for security 14 | emoji = "*" 15 | GitPython = ">=3.1.41" 16 | gql = "*" 17 | idna = ">=3.7" # dependency, pinned for security 18 | PyGithub = ">=2.6.0" 19 | Pygments = "*" 20 | PyYAML = "*" 21 | requests = "*" 22 | requests_toolbelt = "*" 23 | urllib3 = ">=2.0.6" # dependency, pinned for security 24 | 25 | [requires] 26 | python_version = "3.11" 27 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "83af7ba125c977b2e7f27c15c758a2c956a1d70f386be41004871aa2eb2ac626" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.11" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "anyio": { 20 | "hashes": [ 21 | "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", 22 | "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" 23 | ], 24 | "markers": "python_version >= '3.9'", 25 | "version": "==4.9.0" 26 | }, 27 | "asana": { 28 | "hashes": [ 29 | "sha256:117457bd0c4c6e63680539c3355adb974973904a12a4c052e2ba220aec7f1940", 30 | "sha256:542c7a958ccc11f965e3555f5e4261dd02d9936b339f13280add9d521b515903" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.2.3" 34 | }, 35 | "backoff": { 36 | "hashes": [ 37 | "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", 38 | "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8" 39 | ], 40 | "markers": "python_version >= '3.7' and python_version < '4.0'", 41 | "version": "==2.2.1" 42 | }, 43 | "certifi": { 44 | "hashes": [ 45 | "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", 46 | "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" 47 | ], 48 | "markers": "python_version >= '3.6'", 49 | "version": "==2025.1.31" 50 | }, 51 | "cffi": { 52 | "hashes": [ 53 | "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", 54 | "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", 55 | "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", 56 | "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", 57 | "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", 58 | "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", 59 | "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", 60 | "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", 61 | "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", 62 | "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", 63 | "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", 64 | "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", 65 | "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", 66 | "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", 67 | "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", 68 | "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", 69 | "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", 70 | "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", 71 | "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", 72 | "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", 73 | "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", 74 | "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", 75 | "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", 76 | "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", 77 | "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", 78 | "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", 79 | "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", 80 | "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", 81 | "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", 82 | "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", 83 | "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", 84 | "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", 85 | "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", 86 | "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", 87 | "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", 88 | "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", 89 | "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", 90 | "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", 91 | "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", 92 | "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", 93 | "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", 94 | "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", 95 | "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", 96 | "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", 97 | "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", 98 | "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", 99 | "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", 100 | "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", 101 | "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", 102 | "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", 103 | "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", 104 | "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", 105 | "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", 106 | "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", 107 | "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", 108 | "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", 109 | "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", 110 | "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", 111 | "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", 112 | "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", 113 | "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", 114 | "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", 115 | "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", 116 | "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", 117 | "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", 118 | "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", 119 | "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" 120 | ], 121 | "markers": "python_version >= '3.8'", 122 | "version": "==1.17.1" 123 | }, 124 | "charset-normalizer": { 125 | "hashes": [ 126 | "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", 127 | "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", 128 | "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", 129 | "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", 130 | "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", 131 | "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", 132 | "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", 133 | "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", 134 | "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", 135 | "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", 136 | "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", 137 | "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", 138 | "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", 139 | "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", 140 | "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", 141 | "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", 142 | "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", 143 | "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", 144 | "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", 145 | "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", 146 | "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", 147 | "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", 148 | "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", 149 | "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", 150 | "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", 151 | "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", 152 | "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", 153 | "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", 154 | "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", 155 | "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", 156 | "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", 157 | "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", 158 | "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", 159 | "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", 160 | "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", 161 | "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", 162 | "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", 163 | "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", 164 | "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", 165 | "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", 166 | "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", 167 | "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", 168 | "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", 169 | "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", 170 | "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", 171 | "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", 172 | "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", 173 | "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", 174 | "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", 175 | "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", 176 | "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", 177 | "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", 178 | "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", 179 | "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", 180 | "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", 181 | "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", 182 | "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", 183 | "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", 184 | "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", 185 | "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", 186 | "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", 187 | "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", 188 | "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", 189 | "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", 190 | "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", 191 | "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", 192 | "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", 193 | "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", 194 | "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", 195 | "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", 196 | "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", 197 | "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", 198 | "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", 199 | "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", 200 | "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", 201 | "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", 202 | "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", 203 | "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", 204 | "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", 205 | "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", 206 | "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", 207 | "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", 208 | "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", 209 | "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", 210 | "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", 211 | "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", 212 | "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", 213 | "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", 214 | "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", 215 | "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", 216 | "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", 217 | "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" 218 | ], 219 | "markers": "python_version >= '3.7'", 220 | "version": "==3.4.1" 221 | }, 222 | "cryptography": { 223 | "hashes": [ 224 | "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", 225 | "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", 226 | "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", 227 | "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", 228 | "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", 229 | "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", 230 | "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", 231 | "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", 232 | "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", 233 | "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", 234 | "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", 235 | "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", 236 | "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", 237 | "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", 238 | "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", 239 | "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", 240 | "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", 241 | "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", 242 | "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", 243 | "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", 244 | "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", 245 | "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", 246 | "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", 247 | "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", 248 | "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", 249 | "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", 250 | "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", 251 | "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", 252 | "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", 253 | "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", 254 | "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", 255 | "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", 256 | "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", 257 | "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", 258 | "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308" 259 | ], 260 | "index": "pypi", 261 | "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", 262 | "version": "==44.0.2" 263 | }, 264 | "deprecated": { 265 | "hashes": [ 266 | "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", 267 | "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec" 268 | ], 269 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 270 | "version": "==1.2.18" 271 | }, 272 | "emoji": { 273 | "hashes": [ 274 | "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", 275 | "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b" 276 | ], 277 | "index": "pypi", 278 | "markers": "python_version >= '3.7'", 279 | "version": "==2.14.1" 280 | }, 281 | "gitdb": { 282 | "hashes": [ 283 | "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", 284 | "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf" 285 | ], 286 | "markers": "python_version >= '3.7'", 287 | "version": "==4.0.12" 288 | }, 289 | "gitpython": { 290 | "hashes": [ 291 | "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", 292 | "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269" 293 | ], 294 | "index": "pypi", 295 | "markers": "python_version >= '3.7'", 296 | "version": "==3.1.44" 297 | }, 298 | "gql": { 299 | "hashes": [ 300 | "sha256:07e1325b820c8ba9478e95de27ce9f23250486e7e79113dbb7659a442dc13e74", 301 | "sha256:c830ffc38b3997b2a146317b27758305ab3d0da3bde607b49f34e32affb23ba2" 302 | ], 303 | "index": "pypi", 304 | "version": "==3.5.2" 305 | }, 306 | "graphql-core": { 307 | "hashes": [ 308 | "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0", 309 | "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264" 310 | ], 311 | "markers": "python_version >= '3.6' and python_version < '4'", 312 | "version": "==3.2.4" 313 | }, 314 | "idna": { 315 | "hashes": [ 316 | "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", 317 | "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" 318 | ], 319 | "index": "pypi", 320 | "markers": "python_version >= '3.6'", 321 | "version": "==3.10" 322 | }, 323 | "multidict": { 324 | "hashes": [ 325 | "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", 326 | "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", 327 | "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", 328 | "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", 329 | "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", 330 | "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8", 331 | "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", 332 | "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", 333 | "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", 334 | "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", 335 | "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", 336 | "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", 337 | "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", 338 | "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", 339 | "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", 340 | "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", 341 | "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e", 342 | "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b", 343 | "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", 344 | "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", 345 | "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", 346 | "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", 347 | "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", 348 | "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", 349 | "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", 350 | "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", 351 | "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", 352 | "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", 353 | "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", 354 | "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", 355 | "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", 356 | "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", 357 | "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", 358 | "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", 359 | "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", 360 | "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", 361 | "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", 362 | "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", 363 | "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", 364 | "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21", 365 | "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7", 366 | "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", 367 | "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343", 368 | "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", 369 | "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", 370 | "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", 371 | "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", 372 | "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459", 373 | "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6", 374 | "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", 375 | "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", 376 | "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", 377 | "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", 378 | "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", 379 | "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", 380 | "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", 381 | "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", 382 | "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", 383 | "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", 384 | "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", 385 | "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", 386 | "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", 387 | "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", 388 | "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", 389 | "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39", 390 | "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752", 391 | "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", 392 | "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", 393 | "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", 394 | "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", 395 | "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", 396 | "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", 397 | "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f", 398 | "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", 399 | "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f", 400 | "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", 401 | "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", 402 | "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897", 403 | "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887", 404 | "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", 405 | "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", 406 | "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", 407 | "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", 408 | "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", 409 | "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", 410 | "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", 411 | "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", 412 | "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", 413 | "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", 414 | "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", 415 | "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df", 416 | "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", 417 | "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", 418 | "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", 419 | "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", 420 | "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840", 421 | "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", 422 | "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", 423 | "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", 424 | "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", 425 | "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e", 426 | "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2", 427 | "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1", 428 | "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad" 429 | ], 430 | "markers": "python_version >= '3.9'", 431 | "version": "==6.4.3" 432 | }, 433 | "oauthlib": { 434 | "hashes": [ 435 | "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", 436 | "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" 437 | ], 438 | "markers": "python_version >= '3.6'", 439 | "version": "==3.2.2" 440 | }, 441 | "propcache": { 442 | "hashes": [ 443 | "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", 444 | "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", 445 | "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf", 446 | "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", 447 | "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5", 448 | "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c", 449 | "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", 450 | "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", 451 | "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", 452 | "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", 453 | "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", 454 | "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42", 455 | "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035", 456 | "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", 457 | "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", 458 | "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", 459 | "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", 460 | "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", 461 | "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", 462 | "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", 463 | "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", 464 | "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", 465 | "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", 466 | "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833", 467 | "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", 468 | "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", 469 | "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", 470 | "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", 471 | "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", 472 | "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", 473 | "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", 474 | "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", 475 | "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", 476 | "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", 477 | "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566", 478 | "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", 479 | "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908", 480 | "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf", 481 | "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", 482 | "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64", 483 | "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", 484 | "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", 485 | "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", 486 | "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", 487 | "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", 488 | "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5", 489 | "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894", 490 | "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe", 491 | "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", 492 | "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", 493 | "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", 494 | "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", 495 | "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641", 496 | "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", 497 | "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", 498 | "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", 499 | "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", 500 | "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", 501 | "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", 502 | "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", 503 | "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", 504 | "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", 505 | "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", 506 | "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", 507 | "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", 508 | "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294", 509 | "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", 510 | "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", 511 | "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", 512 | "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", 513 | "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", 514 | "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7", 515 | "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", 516 | "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", 517 | "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", 518 | "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", 519 | "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", 520 | "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", 521 | "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", 522 | "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", 523 | "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", 524 | "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", 525 | "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", 526 | "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", 527 | "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", 528 | "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", 529 | "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", 530 | "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6", 531 | "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c", 532 | "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", 533 | "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", 534 | "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", 535 | "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", 536 | "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", 537 | "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", 538 | "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", 539 | "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", 540 | "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5" 541 | ], 542 | "markers": "python_version >= '3.9'", 543 | "version": "==0.3.1" 544 | }, 545 | "pycparser": { 546 | "hashes": [ 547 | "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", 548 | "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" 549 | ], 550 | "markers": "python_version >= '3.8'", 551 | "version": "==2.22" 552 | }, 553 | "pygithub": { 554 | "hashes": [ 555 | "sha256:6f2fa6d076ccae475f9fc392cc6cdbd54db985d4f69b8833a28397de75ed6ca3", 556 | "sha256:b5c035392991cca63959e9453286b41b54d83bf2de2daa7d7ff7e4312cebf3bf" 557 | ], 558 | "index": "pypi", 559 | "markers": "python_version >= '3.8'", 560 | "version": "==2.6.1" 561 | }, 562 | "pygments": { 563 | "hashes": [ 564 | "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", 565 | "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" 566 | ], 567 | "index": "pypi", 568 | "markers": "python_version >= '3.8'", 569 | "version": "==2.19.1" 570 | }, 571 | "pyjwt": { 572 | "extras": [ 573 | "crypto" 574 | ], 575 | "hashes": [ 576 | "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", 577 | "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" 578 | ], 579 | "markers": "python_version >= '3.9'", 580 | "version": "==2.10.1" 581 | }, 582 | "pynacl": { 583 | "hashes": [ 584 | "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", 585 | "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", 586 | "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", 587 | "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", 588 | "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", 589 | "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", 590 | "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", 591 | "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", 592 | "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", 593 | "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" 594 | ], 595 | "markers": "python_version >= '3.6'", 596 | "version": "==1.5.0" 597 | }, 598 | "pyyaml": { 599 | "hashes": [ 600 | "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", 601 | "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", 602 | "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", 603 | "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", 604 | "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", 605 | "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", 606 | "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", 607 | "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", 608 | "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", 609 | "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", 610 | "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", 611 | "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", 612 | "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", 613 | "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", 614 | "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", 615 | "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", 616 | "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", 617 | "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", 618 | "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", 619 | "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", 620 | "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", 621 | "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", 622 | "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", 623 | "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", 624 | "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", 625 | "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", 626 | "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", 627 | "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", 628 | "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", 629 | "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", 630 | "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", 631 | "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", 632 | "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", 633 | "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", 634 | "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", 635 | "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", 636 | "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", 637 | "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", 638 | "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", 639 | "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", 640 | "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", 641 | "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", 642 | "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", 643 | "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", 644 | "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", 645 | "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", 646 | "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", 647 | "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", 648 | "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", 649 | "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", 650 | "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", 651 | "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", 652 | "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" 653 | ], 654 | "index": "pypi", 655 | "markers": "python_version >= '3.8'", 656 | "version": "==6.0.2" 657 | }, 658 | "requests": { 659 | "hashes": [ 660 | "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", 661 | "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" 662 | ], 663 | "index": "pypi", 664 | "markers": "python_version >= '3.8'", 665 | "version": "==2.32.3" 666 | }, 667 | "requests-oauthlib": { 668 | "hashes": [ 669 | "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", 670 | "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" 671 | ], 672 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 673 | "version": "==1.3.1" 674 | }, 675 | "requests-toolbelt": { 676 | "hashes": [ 677 | "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", 678 | "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" 679 | ], 680 | "index": "pypi", 681 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 682 | "version": "==1.0.0" 683 | }, 684 | "smmap": { 685 | "hashes": [ 686 | "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", 687 | "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e" 688 | ], 689 | "markers": "python_version >= '3.7'", 690 | "version": "==5.0.2" 691 | }, 692 | "sniffio": { 693 | "hashes": [ 694 | "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", 695 | "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" 696 | ], 697 | "markers": "python_version >= '3.7'", 698 | "version": "==1.3.1" 699 | }, 700 | "typing-extensions": { 701 | "hashes": [ 702 | "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", 703 | "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" 704 | ], 705 | "markers": "python_version >= '3.8'", 706 | "version": "==4.13.2" 707 | }, 708 | "urllib3": { 709 | "hashes": [ 710 | "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", 711 | "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813" 712 | ], 713 | "index": "pypi", 714 | "markers": "python_version >= '3.9'", 715 | "version": "==2.4.0" 716 | }, 717 | "wrapt": { 718 | "hashes": [ 719 | "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f", 720 | "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", 721 | "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", 722 | "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", 723 | "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", 724 | "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", 725 | "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", 726 | "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", 727 | "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8", 728 | "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", 729 | "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061", 730 | "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", 731 | "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb", 732 | "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", 733 | "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", 734 | "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", 735 | "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", 736 | "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", 737 | "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7", 738 | "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", 739 | "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", 740 | "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", 741 | "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", 742 | "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", 743 | "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", 744 | "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", 745 | "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", 746 | "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a", 747 | "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", 748 | "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", 749 | "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9", 750 | "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", 751 | "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82", 752 | "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9", 753 | "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", 754 | "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", 755 | "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", 756 | "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", 757 | "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", 758 | "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", 759 | "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", 760 | "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", 761 | "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", 762 | "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a", 763 | "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3", 764 | "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a", 765 | "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", 766 | "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", 767 | "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", 768 | "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", 769 | "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", 770 | "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", 771 | "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", 772 | "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", 773 | "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", 774 | "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", 775 | "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2", 776 | "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", 777 | "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", 778 | "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", 779 | "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f", 780 | "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9", 781 | "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04", 782 | "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", 783 | "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9", 784 | "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f", 785 | "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", 786 | "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", 787 | "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", 788 | "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", 789 | "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", 790 | "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", 791 | "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", 792 | "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6", 793 | "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", 794 | "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb", 795 | "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119", 796 | "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b", 797 | "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58" 798 | ], 799 | "markers": "python_version >= '3.8'", 800 | "version": "==1.17.2" 801 | }, 802 | "yarl": { 803 | "hashes": [ 804 | "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", 805 | "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa", 806 | "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61", 807 | "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", 808 | "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2", 809 | "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", 810 | "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", 811 | "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", 812 | "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914", 813 | "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", 814 | "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", 815 | "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569", 816 | "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", 817 | "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", 818 | "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20", 819 | "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", 820 | "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", 821 | "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc", 822 | "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", 823 | "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a", 824 | "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", 825 | "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", 826 | "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", 827 | "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", 828 | "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", 829 | "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", 830 | "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", 831 | "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0", 832 | "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26", 833 | "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", 834 | "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c", 835 | "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", 836 | "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", 837 | "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", 838 | "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a", 839 | "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", 840 | "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", 841 | "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", 842 | "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", 843 | "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", 844 | "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", 845 | "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", 846 | "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", 847 | "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", 848 | "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", 849 | "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", 850 | "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", 851 | "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", 852 | "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe", 853 | "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", 854 | "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", 855 | "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", 856 | "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", 857 | "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62", 858 | "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", 859 | "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", 860 | "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", 861 | "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", 862 | "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb", 863 | "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94", 864 | "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", 865 | "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10", 866 | "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", 867 | "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", 868 | "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", 869 | "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", 870 | "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c", 871 | "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", 872 | "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", 873 | "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195", 874 | "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", 875 | "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", 876 | "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634", 877 | "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", 878 | "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8", 879 | "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", 880 | "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", 881 | "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076", 882 | "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656", 883 | "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", 884 | "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19", 885 | "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", 886 | "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", 887 | "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2", 888 | "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64", 889 | "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", 890 | "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7", 891 | "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995", 892 | "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6", 893 | "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", 894 | "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", 895 | "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487", 896 | "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", 897 | "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", 898 | "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d", 899 | "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", 900 | "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", 901 | "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22", 902 | "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d", 903 | "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c", 904 | "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", 905 | "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5", 906 | "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867", 907 | "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3" 908 | ], 909 | "markers": "python_version >= '3.9'", 910 | "version": "==1.20.0" 911 | } 912 | }, 913 | "develop": { 914 | "black": { 915 | "hashes": [ 916 | "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", 917 | "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", 918 | "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", 919 | "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", 920 | "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", 921 | "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", 922 | "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", 923 | "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", 924 | "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", 925 | "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", 926 | "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", 927 | "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", 928 | "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", 929 | "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", 930 | "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", 931 | "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", 932 | "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", 933 | "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", 934 | "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", 935 | "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", 936 | "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", 937 | "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f" 938 | ], 939 | "index": "pypi", 940 | "markers": "python_version >= '3.9'", 941 | "version": "==25.1.0" 942 | }, 943 | "click": { 944 | "hashes": [ 945 | "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", 946 | "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" 947 | ], 948 | "markers": "python_version >= '3.7'", 949 | "version": "==8.1.8" 950 | }, 951 | "flake8": { 952 | "hashes": [ 953 | "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343", 954 | "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426" 955 | ], 956 | "index": "pypi", 957 | "markers": "python_version >= '3.9'", 958 | "version": "==7.2.0" 959 | }, 960 | "isort": { 961 | "hashes": [ 962 | "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", 963 | "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615" 964 | ], 965 | "index": "pypi", 966 | "markers": "python_full_version >= '3.9.0'", 967 | "version": "==6.0.1" 968 | }, 969 | "mccabe": { 970 | "hashes": [ 971 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 972 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 973 | ], 974 | "markers": "python_version >= '3.6'", 975 | "version": "==0.7.0" 976 | }, 977 | "mypy-extensions": { 978 | "hashes": [ 979 | "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", 980 | "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" 981 | ], 982 | "markers": "python_version >= '3.8'", 983 | "version": "==1.1.0" 984 | }, 985 | "packaging": { 986 | "hashes": [ 987 | "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", 988 | "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" 989 | ], 990 | "markers": "python_version >= '3.8'", 991 | "version": "==25.0" 992 | }, 993 | "pathspec": { 994 | "hashes": [ 995 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 996 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 997 | ], 998 | "markers": "python_version >= '3.8'", 999 | "version": "==0.12.1" 1000 | }, 1001 | "platformdirs": { 1002 | "hashes": [ 1003 | "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", 1004 | "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351" 1005 | ], 1006 | "markers": "python_version >= '3.9'", 1007 | "version": "==4.3.7" 1008 | }, 1009 | "pycodestyle": { 1010 | "hashes": [ 1011 | "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", 1012 | "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae" 1013 | ], 1014 | "markers": "python_version >= '3.9'", 1015 | "version": "==2.13.0" 1016 | }, 1017 | "pyflakes": { 1018 | "hashes": [ 1019 | "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", 1020 | "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b" 1021 | ], 1022 | "markers": "python_version >= '3.9'", 1023 | "version": "==3.3.2" 1024 | } 1025 | } 1026 | } 1027 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creative Commons (CC) Open Source Scripts 2 | 3 | These are scripts used to maintain various pieces of CC's open source community 4 | infrastructure. 5 | 6 | 7 | ## Status 8 | 9 | - [![Sync Community Teams with GitHub][teams_badge]][teams_link] 10 | - [![Manage issues and pull requests in projects][issues_badge]][issues_link] 11 | - [![Normalize Repos][norm_badge]][norm_link] 12 | - [![Push data to CC Open Source][data_badge]][data_link] 13 | - [![Enable workflows][enable_badge]][enable_link] 14 | 15 | [teams_badge]: https://github.com/creativecommons/ccos-scripts/actions/workflows/sync_community_teams.yml/badge.svg 16 | [teams_link]: #sync-community-teams-with-github 17 | [issues_badge]: https://github.com/creativecommons/ccos-scripts/actions/workflows/manage_issues.yml/badge.svg 18 | [issues_link]: #manage-issues-and-pull-requests-in-projects 19 | [norm_badge]: https://github.com/creativecommons/ccos-scripts/actions/workflows/normalize_repos.yml/badge.svg 20 | [norm_link]: #normalize-repos 21 | [data_badge]: https://github.com/creativecommons/ccos-scripts/actions/workflows/push_data_to_ccos.yml/badge.svg 22 | [data_link]: #push-data-to-cc-open-source 23 | [enable_badge]: https://github.com/creativecommons/ccos-scripts/actions/workflows/enable_workflows.yml/badge.svg 24 | [enable_link]: #enable-workflows 25 | 26 | 27 | ## Code of conduct 28 | 29 | [`CODE_OF_CONDUCT.md`][org-coc]: 30 | > The Creative Commons team is committed to fostering a welcoming community. 31 | > This project and all other Creative Commons open source projects are governed 32 | > by our [Code of Conduct][code_of_conduct]. Please report unacceptable 33 | > behavior to [conduct@creativecommons.org](mailto:conduct@creativecommons.org) 34 | > per our [reporting guidelines][reporting_guide]. 35 | 36 | [org-coc]: https://github.com/creativecommons/.github/blob/main/CODE_OF_CONDUCT.md 37 | [code_of_conduct]: https://opensource.creativecommons.org/community/code-of-conduct/ 38 | [reporting_guide]: https://opensource.creativecommons.org/community/code-of-conduct/enforcement/ 39 | 40 | 41 | ## Contributing 42 | 43 | See [`CONTRIBUTING.md`][org-contrib]. 44 | 45 | [org-contrib]: https://github.com/creativecommons/.github/blob/main/CONTRIBUTING.md 46 | 47 | 48 | ## Workflows 49 | 50 | The following workflows are ordered by schedule frequency and start time. 51 | 52 | 53 | ### Sync Community Teams with GitHub 54 | 55 | | **Workflow** | | | 56 | | -- | --: | --- | 57 | | | Schedule: | Hourly at 30 minutes past the hour (`**:30`) | 58 | | | YAML: | [`sync_community_teams.yml`][sync_teams_yml] | 59 | | **Script** | | | 60 | | | File: | [`sync_community_teams.py`][teams_file] | 61 | | | Common Modules: | [`ccos/`](ccos/) | 62 | | | Specific Modules: | [`ccos/norm/`](ccos/norm/) | 63 | | **Env** | | | 64 | | | Required: | `ADMIN_GITHUB_TOKEN` | 65 | 66 | This creates GitHub teams for the Community teams and updates their membership 67 | based on the [`community_team_members.json`][databag] Lektor databag. 68 | - The databag is used to: 69 | - create the [Community Team Members — Creative Commons Open 70 | Source][ctlistpage] page 71 | - configure GitHub team memberships and repository permissions 72 | - The databag is kept up-to-date by [Push data to CC Open 73 | Source](#push-data-to-cc-open-source), below 74 | 75 | [sync_teams_yml]: .github/workflows/sync_community_teams.yml 76 | [teams_file]: sync_community_teams.py 77 | [databag]: https://github.com/creativecommons/ccos-website-source/blob/master/databags/community_team_members.json 78 | [ctlistpage]: https://opensource.creativecommons.org/community/community-team/members/ 79 | 80 | 81 | ### Manage new issues and pull requests in projects 82 | 83 | | **Workflow** | | | 84 | | -- | --: | --- | 85 | | | Schedule: | Hourly at 45 minutes past the hour (`**:45`) | 86 | | | YAML: | [`manage_issues.yml`][manage_issues] | 87 | | **Script** | | | 88 | | | File: | [`manage_new_issues_and_pull_requests.py`][manage_new_issues] | 89 | | | Common Modules: | [`ccos/`](ccos/) | 90 | | **Env** | | | 91 | | | Required: | `ADMIN_GITHUB_TOKEN` | 92 | 93 | This manages new issues and pull requests to ensure they are properly tracked 94 | in a GitHub project: 95 | - [possumbilities project][proj_possumbilities]: _Web Development and Web 96 | Support_ 97 | - [Shafiya-Heena project][proj-shafiya-heena]: _IT Support, Platforms, and 98 | Systems_ 99 | - [TimidRobot project][proj_timidrobot]: _Application Programming and 100 | Management_ 101 | 102 | [manage_issues]: .github/workflows/manage_issues.yml 103 | [manage_new_issues]: manage_new_issues_and_pull_requests.py 104 | [proj_possumbilities]: https://github.com/orgs/creativecommons/projects/23/views/1 105 | [proj-shafiya-heena]: https://github.com/orgs/creativecommons/projects/22/views/1 106 | [proj_timidrobot]: https://github.com/orgs/creativecommons/projects/15/views/1 107 | 108 | 109 | ### Normalize Repos 110 | 111 | | **Workflow** | | | 112 | | -- | --: | --- | 113 | | | Schedule: | Hourly at 45 minutes past the hour (`**:45`) | 114 | | | YAML: | [`normalize_repos.yml`][norm_pr_yml] | 115 | | **Script** | | | 116 | | | File: | [`normalize_repos.py`][norm_file] | 117 | | | Common Modules: | [`ccos/`](ccos/) | 118 | | | Specific Modules: | [`ccos/norm/`](ccos/norm/) | 119 | | **Env** | | | 120 | | | Required: | `ADMIN_GITHUB_TOKEN` | 121 | 122 | This ensures that all active repositories in the creativecommons 123 | GitHub organization are consistent in the following ways: 124 | - They have all the labels defined in `labels.yml` present. 125 | - They have standard branch protections set up (with some exceptions). 126 | 127 | This will only update color and description of existing labels or create 128 | new labels. It will never delete labels. 129 | 130 | [norm_pr_yml]: .github/workflows/normalize_repos.yml 131 | [norm_file]: normalize_repos.py 132 | 133 | 134 | ### Push data to CC Open Source 135 | 136 | | **Workflow** | | | 137 | | -- | --: | --- | 138 | | | Schedule: | Daily at midnight:15 (`00:15`) | 139 | | | YAML: | [`push_data_to_ccos.yml`][push_ccos_yml] | 140 | | **Script** | | | 141 | | | File: | [`push_data_to_ccos.py`][data_file] | 142 | | | Common Modules: | [`ccos/`](ccos/) | 143 | | | Specific Modules: | [`ccos/data/`](ccos/data/) | 144 | | **Env** | | | 145 | | | Required: | `ADMIN_ASANA_TOKEN` | 146 | | | Required: | `ADMIN_GITHUB_TOKEN` | 147 | 148 | This retreives data from Asana, formats it as a lektor databag, and pushes it 149 | to CC Open Source website source repository: 150 | - Data Source: [Community Team Tracking - Asana][asana] (limited access) 151 | - Data Destination: 152 | - [`creativecommons/ccos-website-source`][ccos_source] 153 | - [`databags/community_team_members.json`][db_community] 154 | - [`databags/repos.json`][db_repos] 155 | 156 | The destination data is used by the following pages: 157 | - [Community Team Members — Creative Commons Open Source][ctlistpage] 158 | - [Open Source Projects — Creative Commons Open Source][osproj] 159 | 160 | [push_ccos_yml]: .github/workflows/push_data_to_ccos.yml 161 | [data_file]: push_data_to_ccos.py 162 | [ctlistpage]: https://opensource.creativecommons.org/community/community-team/members/ 163 | [osproj]: https://opensource.creativecommons.org/contributing-code/projects/ 164 | [asana]: https://app.asana.com/0/1172465506923657/list 165 | [ccos_source]: https://github.com/creativecommons/ccos-website-source 166 | [db_community]: https://github.com/creativecommons/ccos-website-source/blob/main/databags/community_team_members.json 167 | [db_repos]: https://github.com/creativecommons/ccos-website-source/blob/main/databags/repos.json 168 | 169 | 170 | ### Eanble Workflows 171 | 172 | | **Workflow** | | | 173 | | -- | --: | --- | 174 | | | Schedule: | Monthly on the 1st at 22:22 | 175 | | | YAML: | [`enable_workflows.yml`][enable_workflows_yml] | 176 | | **Script** | | | 177 | | | File: | [`enable_workflows.py`][enable_workflows_file] | 178 | | | Common Modules: | [`ccos/`](ccos/) | 179 | | **Env** | | | 180 | | | Required: | `ADMIN_GITHUB_TOKEN` | 181 | 182 | Enable GitHub Action workflows for specified repositories (ensures that they 183 | are not disabled due to inactivity). 184 | 185 | For more information, see [Prevent scheduled GitHub Actions from becoming disabled - Stack Overflow][prevent_scheduled]. 186 | 187 | [enable_workflows_yml]: .github/workflows/enable_workflows.yml 188 | [enable_workflows_file]: enable_workflows.py 189 | [prevent_scheduled]: https://stackoverflow.com/questions/67184368/prevent-scheduled-github-actions-from-becoming-disabled 190 | 191 | 192 | ## Environment Variables 193 | 194 | - `ADMIN_ASANA_TOKEN`: Asana token with access to the Creative Commons Asana 195 | organization 196 | - `ADMIN_GITHUB_TOKEN`: GitHub token with admin permissions to the 197 | `creativecommons` GitHub organization 198 | 199 | 200 | ## :robot: Automation Authorship 201 | 202 | Scripts that commit code or automatically reply to pull requests and issues 203 | need to be associated with a GitHub user account. Creative Commons maintains a 204 | [cc-open-source-bot](https://github.com/cc-open-source-bot) user for this 205 | purpose. This is useful for a few reasons: 206 | 207 | - It's ethically important that our community members know when they are 208 | talking to a bot instead of a human. 209 | - It makes it easy to audit our automations in the future, because all commits 210 | and messages will be associated with the single @cc-open-source-bot user 211 | account via the GitHub search, api, etc. 212 | - We won't need to update automations when there are changes to staff or 213 | volunteers. 214 | 215 | Using this bot clearly communicates when a commit, comment, or action was 216 | performed by an automation. For example, here is some configuration for a 217 | workflow using the [Add & Commit](https://github.com/EndBug/add-and-commit) 218 | GitHub Action: 219 | 220 | ```yml 221 | # ...other settings here 222 | - name: Commit changes 223 | uses: EndBug/add-and-commit@v4 224 | with: 225 | author_name: cc-open-source-bot 226 | author_email: opensource@creativecommons.org 227 | message: "Deploy site" 228 | add: "./example-directory" 229 | ``` 230 | 231 | 232 | ## Development 233 | 234 | Local development and testing is facilitated by helper scripts: 235 | - `./dev/tools.sh`: Checks and updates Python formatting 236 | - `.dev/test.sh`: Uses act and Docker to test workflows 237 | - [nektos/act](https://github.com/nektos/act): _Run your GitHub Actions 238 | locally 🚀_ 239 | 240 | 241 | ### Python Dependencies 242 | 243 | - [Asana/python-asana][python-asana]: _Official Python client library for the 244 | Asana API v1_ 245 | - [carpedm20/emoji][emoji]: _e_moji terminal output for Python_ 246 | - [gitpython-developers/GitPython][gitpython]: _GitPython is a python library 247 | used to interact with Git repositories._ 248 | - [graphql-python/gql][pygql]: _A GraphQL client in Python_ 249 | - [PyGithub/PyGithub][pygithub]: _Typed interactions with the GitHub API v3_ 250 | - [PyYAML][pyyaml]: _a full-featured YAML framework for the Python 251 | programming language_ 252 | - [Requests][requests]: _HTTP for Humans™_ 253 | 254 | [python-asana]: https://github.com/asana/python-asana 255 | [emoji]: https://github.com/carpedm20/emoji/ 256 | [gitpython]: https://github.com/gitpython-developers/GitPython 257 | [pygql]: https://github.com/graphql-python/gql 258 | [pygithub]: https://github.com/pygithub/pygithub 259 | [pyyaml]: https://pyyaml.org/ 260 | [requests]: https://requests.readthedocs.io/en/latest/ 261 | 262 | 263 | ### GitHub GraphQL API 264 | 265 | - [Using the API to manage Projects - GitHub Docs][projectsv2api] 266 | - [Forming calls with GraphQL - GitHub Docs][formingcalls] 267 | 268 | [projectsv2api]: https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects 269 | [formingcalls]: https://docs.github.com/en/graphql/guides/forming-calls-with-graphql 270 | 271 | 272 | ## License 273 | 274 | - [`LICENSE`](LICENSE) (Expat/[MIT][mit] License) 275 | 276 | [mit]: http://www.opensource.org/licenses/MIT "The MIT License | Open Source Initiative" 277 | 278 | 279 | ### GitHub GraphQL API schema 280 | 281 | The GitHub GraphQL API public schema 282 | ([`ccos/schema.docs.graphql`][graphqlschema]) was downloaded from [Public 283 | schema - GitHub Docs][publicschema] and is not within scope of the Expat/MIT 284 | license of this project. 285 | 286 | [graphqlschema]: ccos/schema.docs.graphql 287 | [publicschema]: https://docs.github.com/en/graphql/overview/public-schema 288 | -------------------------------------------------------------------------------- /_ARCHIVE/remove_hacktoberfest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This script removes all Hacktoberfest issues labels and related repository 5 | topics. 6 | """ 7 | 8 | # Standard library 9 | import argparse 10 | import sys 11 | import traceback 12 | 13 | # Third-party 14 | from github import UnknownObjectException 15 | 16 | # First-party/Local 17 | import ccos.log 18 | from ccos import gh_utils 19 | 20 | LOG = ccos.log.setup_logger() 21 | 22 | 23 | def setup(): 24 | """Instantiate and configure argparse and logging. 25 | 26 | Return argsparse namespace. 27 | """ 28 | ap = argparse.ArgumentParser(description=__doc__) 29 | ap.add_argument( 30 | "-r", 31 | "--repo", 32 | "--repository", 33 | action="append", 34 | help="select repository or repositories to update from those fetched" 35 | " from GitHub (may be specified multiple times)", 36 | metavar="REPO", 37 | dest="repos", 38 | ) 39 | args = ap.parse_args() 40 | return args 41 | 42 | 43 | def remove_hacktoberfest_label(repos): 44 | for repo in repos: 45 | try: 46 | hacktober_label = repo.get_label("Hacktoberfest") 47 | except UnknownObjectException: 48 | continue 49 | hacktober_label.delete() 50 | LOG.info(f"{repo.name}: deleted issues label: Hacktoberfest") 51 | 52 | 53 | def remove_hacktoberfest_topic(repos): 54 | for repo in repos: 55 | old_topics = repo.get_topics() 56 | old_topics.sort() 57 | new_topics = [] 58 | hacktober_topics = [] 59 | for topic in old_topics: 60 | if "hacktober" not in topic.lower(): 61 | new_topics.append(topic) 62 | else: 63 | hacktober_topics.append(topic) 64 | new_topics.sort() 65 | hacktober_topics.sort() 66 | hacktober_topics = ", ".join(hacktober_topics) 67 | if old_topics != new_topics: 68 | repo.replace_topics(new_topics) 69 | LOG.info( 70 | f"{repo.name}: removed Hacktoberfest related topics:" 71 | f" {hacktober_topics}" 72 | ) 73 | 74 | 75 | def main(): 76 | args = setup() 77 | LOG.info("Starting normalization") 78 | repos = gh_utils.get_select_repos(args) 79 | remove_hacktoberfest_label(repos) 80 | remove_hacktoberfest_topic(repos) 81 | 82 | 83 | if __name__ == "__main__": 84 | try: 85 | main() 86 | except SystemExit as e: 87 | sys.exit(e.code) 88 | except KeyboardInterrupt: 89 | LOG.info("Halted via KeyboardInterrupt.") 90 | sys.exit(130) 91 | except Exception: 92 | LOG.error(f"Unhandled exception: {traceback.format_exc()}") 93 | sys.exit(1) 94 | -------------------------------------------------------------------------------- /ccos/data/asana.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import logging 3 | import os 4 | import sys 5 | 6 | # Third-party 7 | import asana 8 | 9 | # To see workspace GID, log into Asana and then view: 10 | # https://app.asana.com/api/1.0/workspaces 11 | ASANA_WORKSPACE_GID = "133733285600979" 12 | # To see project GIDs, log into Asana and then view: 13 | # https://app.asana.com/api/1.0/projects 14 | # 15 | # To see "Community Team Tracking" project section GIDs, log into Asana and 16 | # then view: 17 | # https://app.asana.com/api/1.0/projects/1172465506923657/sections 18 | ASANA_SECTION_GID = "1172465506923661" 19 | LOG = logging.root 20 | 21 | 22 | def setup_asana_client(): 23 | LOG.info("Setting up Asana client...") 24 | try: 25 | asana_token = os.environ["ADMIN_ASANA_TOKEN"] 26 | except KeyError: 27 | LOG.critical("missin ADMIN_ASANA_TOKEN environment variable") 28 | sys.exit(1) 29 | asana_client = asana.Client.access_token(asana_token) 30 | asana_client.headers = {"asana-enable": "new_goal_memberships"} 31 | try: 32 | # Perform simple API operation to test authentication 33 | asana_client.workspaces.get_workspace(ASANA_WORKSPACE_GID) 34 | except asana.error.NoAuthorizationError as e: 35 | LOG.critical(f"{e.status} {e.message} (is ADMIN_ASANA_TOKEN valid?)") 36 | sys.exit(1) 37 | LOG.success("done.") 38 | return asana_client 39 | 40 | 41 | def get_asana_team_members(asana_client): 42 | LOG.info("Get Team Members...") 43 | team_members = asana_client.tasks.find_by_section( 44 | ASANA_SECTION_GID, opt_fields=["name", "custom_fields"] 45 | ) 46 | LOG.success("done.") 47 | return team_members 48 | -------------------------------------------------------------------------------- /ccos/data/get_community_team_data.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import logging 3 | import sys 4 | 5 | LOG = logging.root 6 | 7 | 8 | def generate_databag(team_members): 9 | """ 10 | This method pulls the team members from Asana and 11 | loads them into the databag after a little 12 | formatting. The output of this method still needs 13 | pruning. The databag schema is below. 14 | 15 | databag schema 16 | { 17 | "projects": [ 18 | { 19 | "name": "", 20 | "repos": "", 21 | "members": [ 22 | { 23 | "name": "", 24 | "role": "", 25 | "github: "" 26 | }, 27 | ... 28 | ] 29 | }, 30 | ... 31 | ], 32 | "community_builders": [ 33 | { 34 | "name": "", 35 | "role": "", 36 | "github: "" 37 | }, 38 | ... 39 | ] 40 | } 41 | """ 42 | databag = {"projects": [], "community_builders": []} 43 | LOG.info("Processing team members...") 44 | for member in team_members: 45 | if member["name"] == "": 46 | continue # Sometimes blank names come up 47 | role = get_custom_field(member, "Role") 48 | github = get_custom_field(member, "GitHub") 49 | if role.startswith("Community"): 50 | databag["community_builders"].append( 51 | {"name": member["name"], "role": role, "github": github} 52 | ) 53 | else: 54 | project_name = get_custom_field(member, "Project Name") 55 | seen_projects = [] 56 | 57 | if project_name not in seen_projects: 58 | databag["projects"].append( 59 | { 60 | "name": project_name, 61 | "members": [], 62 | "repos": get_custom_field(member, "Repo(s)"), 63 | } 64 | ) 65 | seen_projects.append(project_name) 66 | 67 | for project in databag["projects"]: 68 | if project["name"] == project_name: 69 | project["members"].append( 70 | { 71 | "name": member["name"], 72 | "role": role, 73 | "github": github, 74 | } 75 | ) 76 | break 77 | 78 | LOG.success("done.") 79 | return databag 80 | 81 | 82 | def sort_databag(databag): 83 | """ 84 | This function orderes the member according to their roles 85 | """ 86 | project_priority = { 87 | "Project Maintainer": 1, 88 | "Project Core Committer": 2, 89 | "Project Collaborator": 3, 90 | "Project Contributor": 4, 91 | } 92 | community_builders_priority = { 93 | "Community Maintainer": 1, 94 | "Community Collaborator": 2, 95 | } 96 | 97 | for first_order_key in databag: 98 | if first_order_key == "projects": 99 | for project in databag["projects"]: 100 | member = project["members"] 101 | member.sort(key=lambda x: x["name"]) 102 | member.sort(key=lambda x: project_priority[x["role"]]) 103 | 104 | elif first_order_key == "community_builders": 105 | community_builder = databag["community_builders"] 106 | community_builder.sort(key=lambda x: x["name"]) 107 | community_builder.sort( 108 | key=lambda x: community_builders_priority[x["role"]] 109 | ) 110 | return databag 111 | 112 | 113 | def prune_databag(databag): 114 | """ 115 | Sometimes empty projects find their way into the databag. 116 | This function prunes out the empty ones. 117 | """ 118 | pruned = { 119 | "projects": [], 120 | "community_builders": databag["community_builders"], 121 | } 122 | 123 | for project in databag["projects"]: 124 | if len(project["members"]) > 0: 125 | pruned["projects"].append(project) 126 | 127 | return pruned 128 | 129 | 130 | def verify_databag(databag, repo_names): 131 | """ 132 | Ensure all repository names are accurate. 133 | """ 134 | for project in databag["projects"]: 135 | databag_repos = project["repos"].split(",") 136 | for databag_repo in databag_repos: 137 | if databag_repo not in repo_names: 138 | LOG.error( 139 | f'"{project["name"]}" contains invalid reposiotry:' 140 | f' "{databag_repo}"', 141 | ) 142 | sys.exit(1) 143 | return databag 144 | 145 | 146 | def get_custom_field(task, field_name): 147 | """ 148 | Gets the value of a custom field 149 | """ 150 | for field in task["custom_fields"]: 151 | if field["name"] == field_name: 152 | if field["type"] == "enum": 153 | return field["enum_value"]["name"] 154 | elif field["type"] == "text": 155 | return field["text_value"] 156 | 157 | 158 | def get_community_team_data(team_members, repo_names): 159 | databag = generate_databag(team_members) 160 | databag = prune_databag(databag) 161 | databag = verify_databag(databag, repo_names) 162 | databag = sort_databag(databag) 163 | return databag 164 | -------------------------------------------------------------------------------- /ccos/data/get_repo_data.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import logging 3 | 4 | # Third-party 5 | import emoji 6 | import yaml 7 | from github.GithubException import GithubException, UnknownObjectException 8 | 9 | CC_METADATA_FILE_NAME = ".cc-metadata.yml" 10 | LOG = logging.root 11 | 12 | 13 | def get_repositories(organization): 14 | LOG.info("Getting CC's repos...") 15 | repos = organization.get_repos() 16 | return repos 17 | 18 | 19 | def get_repo_github_data(repo): 20 | LOG.info("Getting data for this repo...") 21 | repo_github_data = { 22 | "id": repo.id, 23 | "name": repo.name, 24 | "url": repo.html_url, 25 | "description": ( 26 | emoji.emojize(repo.description) if repo.description else "" 27 | ), 28 | "website": repo.homepage, 29 | "language": repo.language, 30 | "created": repo.created_at.isoformat(), 31 | } 32 | try: 33 | license = repo.get_license() 34 | except UnknownObjectException: 35 | license = None 36 | if license: 37 | repo_github_data["license"] = { 38 | "name": license.license.name, 39 | "url": license.html_url, 40 | } 41 | else: 42 | repo_github_data["license"] = None 43 | return repo_github_data 44 | 45 | 46 | def get_repo_cc_metadata(repo): 47 | LOG.info("Getting CC metadata for this repo...") 48 | try: 49 | cc_metadata_file = repo.get_contents(CC_METADATA_FILE_NAME) 50 | except (UnknownObjectException, GithubException): 51 | return {} 52 | cc_metadata = yaml.safe_load(cc_metadata_file.decoded_content) 53 | if "technologies" in cc_metadata: 54 | cc_metadata["technologies"] = [ 55 | technology.strip() 56 | for technology in cc_metadata["technologies"].split(",") 57 | ] 58 | return cc_metadata 59 | 60 | 61 | def get_repo_data_list(repos): 62 | repo_data_list = [] 63 | count = 1 64 | total = repos.totalCount 65 | 66 | for repo in repos: 67 | LOG.info(f"Processing {count} of {total} – {repo.name}") 68 | if not repo.private: 69 | repo_cc_metadata = get_repo_cc_metadata(repo) 70 | is_engineering_project = repo_cc_metadata.get( 71 | "engineering_project", True 72 | ) 73 | if is_engineering_project: 74 | repo_github_data = get_repo_github_data(repo) 75 | if "slack" not in repo_cc_metadata: 76 | repo_cc_metadata["slack"] = "" 77 | repo_data = {**repo_github_data, **repo_cc_metadata} 78 | repo_data_list.append(repo_data) 79 | else: 80 | LOG.info("Not an active engineering project, skipping") 81 | count += 1 82 | return sorted(repo_data_list, key=lambda k: k["name"].lower()) 83 | 84 | 85 | def get_repo_data_dict(repo_data_list): 86 | # This is needed because Lektor needs a top level object (not array) in the 87 | # JSON file. 88 | return {"repos": repo_data_list} 89 | 90 | 91 | def get_repo_data(gh_org_cc): 92 | repos = get_repositories(gh_org_cc) 93 | repo_data_list = get_repo_data_list(repos) 94 | data = get_repo_data_dict(repo_data_list) 95 | return data 96 | 97 | 98 | def get_repo_names(gh_org_cc): 99 | repos = get_repositories(gh_org_cc) 100 | names = [] 101 | for repo in repos: 102 | names.append(repo.name) 103 | return names 104 | -------------------------------------------------------------------------------- /ccos/data/push_data_via_git.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import json 3 | import logging 4 | import os 5 | from tempfile import TemporaryDirectory 6 | 7 | # Third-party 8 | import git 9 | 10 | # First-party/Local 11 | from ccos.gh_utils import GITHUB_ORGANIZATION, get_credentials 12 | 13 | GITHUB_REPO_NAME = "ccos-website-source" 14 | GIT_USER_EMAIL = "cc-creativecommons-github-io-bot@creativecommons.org" 15 | GIT_USER_NAME = "CC creativecommons.github.io Bot" 16 | JSON_FILE_DIR = "databags" 17 | LOG = logging.root 18 | 19 | 20 | def set_up_repo(git_working_dir): 21 | github_username, github_token = get_credentials() 22 | github_repo_url_with_credentials = ( 23 | f"https://{github_username}:{github_token}" 24 | f"@github.com/{GITHUB_ORGANIZATION}/{GITHUB_REPO_NAME}.git" 25 | ) 26 | if not os.path.isdir(git_working_dir): 27 | LOG.info("Cloning repo...") 28 | repo = git.Repo.clone_from( 29 | url=github_repo_url_with_credentials, to_path=git_working_dir 30 | ) 31 | else: 32 | LOG.info("Setting up repo...") 33 | repo = git.Repo(git_working_dir) 34 | origin = repo.remotes.origin 35 | LOG.info("Pulling latest code...") 36 | origin.pull() 37 | 38 | 39 | def set_up_git_user(): 40 | LOG.info("Setting up git user...") 41 | os.environ["GIT_AUTHOR_NAME"] = GIT_USER_NAME 42 | os.environ["GIT_AUTHOR_EMAIL"] = GIT_USER_EMAIL 43 | os.environ["GIT_COMMITTER_NAME"] = GIT_USER_NAME 44 | os.environ["GIT_COMMITTER_EMAIL"] = GIT_USER_EMAIL 45 | 46 | 47 | def generate_json_file(git_working_dir, data, filename): 48 | LOG.info("Generating JSON file...") 49 | json_filename = os.path.join(git_working_dir, JSON_FILE_DIR, filename) 50 | with open(json_filename, "w") as json_file: 51 | json.dump(data, json_file, sort_keys=True, indent=4) 52 | return json_filename 53 | 54 | 55 | def commit_and_push_changes(git_working_dir, json_filename): 56 | repo = git.Repo(git_working_dir) 57 | git_diff = repo.index.diff(None) 58 | if git_diff: 59 | repo.index.add(items=f"{json_filename}") 60 | repo.index.commit(message="Syncing new data changes.") 61 | origin = repo.remotes.origin 62 | LOG.info("Pushing latest code...") 63 | origin.push() 64 | else: 65 | LOG.info("No changes to push...") 66 | 67 | 68 | def push_data(data, filename): 69 | with TemporaryDirectory() as temp_dir: 70 | git_working_dir = os.path.join(temp_dir, GITHUB_REPO_NAME) 71 | set_up_repo(git_working_dir) 72 | set_up_git_user() 73 | json_filename = generate_json_file(git_working_dir, data, filename) 74 | commit_and_push_changes(git_working_dir, json_filename) 75 | -------------------------------------------------------------------------------- /ccos/gh_utils.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import logging 3 | import os 4 | import re 5 | import sys 6 | import textwrap 7 | 8 | # Third-party 9 | from github import Github 10 | from github.GithubException import BadCredentialsException 11 | from gql import Client, gql 12 | from gql.transport.requests import RequestsHTTPTransport 13 | from gql.transport.requests import log as gql_requests_log 14 | from graphql.error.syntax_error import GraphQLSyntaxError 15 | from pygments import highlight 16 | from pygments.formatters import TerminalFormatter 17 | from pygments.lexers import GraphQLLexer 18 | from urllib3.util.retry import Retry 19 | 20 | GITHUB_ORGANIZATION = "creativecommons" 21 | GITHUB_RETRY_STATUS_FORCELIST = [ 22 | 408, # Request Timeout 23 | 429, # Too Many Requests 24 | 500, # Internal Server Error 25 | 502, # Bad Gateway 26 | 503, # Service Unavailable 27 | 504, # Gateway Timeout 28 | ] 29 | GITHUB_USERNAME_DEFAULT = "cc-creativecommons-github-io-bot" 30 | LOG = logging.root 31 | gql_requests_log.setLevel(logging.WARNING) 32 | 33 | 34 | def get_credentials(): 35 | try: 36 | github_token = os.environ["ADMIN_GITHUB_TOKEN"] 37 | except KeyError: 38 | LOG.critical("missing ADMIN_GITHUB_TOKEN environment variable") 39 | sys.exit(1) 40 | try: 41 | github_username = os.environ["ADMIN_GITHUB_USERNAME"] 42 | except KeyError: 43 | github_username = GITHUB_USERNAME_DEFAULT 44 | return github_username, github_token 45 | 46 | 47 | def gql_query(query): 48 | try: 49 | validated_query = gql(query) 50 | except GraphQLSyntaxError as e: 51 | query_formatted = highlight( 52 | textwrap.indent(textwrap.dedent(query), " "), 53 | GraphQLLexer(), 54 | TerminalFormatter(), 55 | ) 56 | error_formatted = textwrap.indent(f"{e}", " ") 57 | LOG.error( 58 | f"Invalid GraphQL syntax:\n{query_formatted}\n{error_formatted}" 59 | ) 60 | sys.exit(1) 61 | return validated_query 62 | 63 | 64 | def setup_github_gql_client(): 65 | _, github_token = get_credentials() 66 | LOG.info("Setting up GitHub GraphQL API client") 67 | transport = RequestsHTTPTransport( 68 | url="https://api.github.com/graphql", 69 | headers={"Authorization": f"bearer {github_token}"}, 70 | timeout=10, 71 | retries=5, 72 | retry_backoff_factor=10, 73 | retry_status_forcelist=GITHUB_RETRY_STATUS_FORCELIST, 74 | ) 75 | with open("ccos/schema.docs.graphql") as file_obj: 76 | gh_schema = file_obj.read() 77 | github_gql_client = Client(transport=transport, schema=gh_schema) 78 | return github_gql_client 79 | 80 | 81 | def setup_github_rest_client(): 82 | _, github_token = get_credentials() 83 | LOG.info("Setting up GitHub Rest API client") 84 | # TODO: Remove retry parameter (urllib3.util.retry.Retry object) once we 85 | # are using PyGithub v2.0 86 | # https://github.com/creativecommons/ccos-scripts/issues/179 87 | retry = Retry( 88 | # try again after 5, 10, 20, 40, 80 seconds 89 | # for specified HTTP status codes 90 | total=5, 91 | backoff_factor=10, 92 | status_forcelist=GITHUB_RETRY_STATUS_FORCELIST, 93 | allowed_methods={ 94 | "DELETE", 95 | "GET", 96 | "HEAD", 97 | "OPTIONS", 98 | "POST", 99 | "PUT", 100 | "TRACE", 101 | }, 102 | ) 103 | github_rest_client = Github(login_or_token=github_token, retry=retry) 104 | return github_rest_client 105 | 106 | 107 | def get_cc_organization(github_client=None): 108 | if github_client is None: 109 | github_client = setup_github_rest_client() 110 | LOG.info("Getting CC's GitHub organization...") 111 | try: 112 | gh_org_cc = github_client.get_organization(GITHUB_ORGANIZATION) 113 | except BadCredentialsException as e: 114 | LOG.critical( 115 | f"{e.status} {e.data['message']} (see" 116 | f" {e.data['documentation_url']})" 117 | ) 118 | sys.exit(1) 119 | LOG.success("done.") 120 | return gh_org_cc 121 | 122 | 123 | def get_select_repos(args, gh_org_cc=None): 124 | if gh_org_cc is None: 125 | gh_org_cc = get_cc_organization() 126 | LOG.info("Get select GitHub repositories") 127 | LOG.change_indent(-1) 128 | repos = list(gh_org_cc.get_repos()) 129 | LOG.change_indent(+1) 130 | # Skip archived repos 131 | repos_selected = [] 132 | for repo in repos: 133 | if not repo.archived: 134 | repos_selected.append(repo) 135 | repos = repos_selected 136 | # Skip non-selected repos 137 | if args.repos: 138 | repos_selected = [] 139 | for repo in repos: 140 | if repo.name in args.repos: 141 | repos_selected.append(repo) 142 | repos = repos_selected 143 | if not repos: 144 | raise Exception( 145 | "Specified repositories do not include any valid" 146 | f" repositories: {args.repos}" 147 | ) 148 | repos.sort(key=lambda repo: repo.name) 149 | return repos 150 | 151 | 152 | def get_team_slug_name(project_name, role): 153 | """ 154 | Get the team name and team slug based on GitHub's naming scheme. By 155 | convention, teams are named in a well-defined . 156 | 157 | team name schema 158 | CT: 159 | 160 | team slug schema 161 | ct-- 162 | 163 | @param project_name: the name of the project to which the team belongs 164 | @param role: the role held by folks in the team 165 | @return: the slug and name of the team 166 | """ 167 | sanitized_role = pluralized(role).replace("Project ", "") 168 | team_name = f"CT: {project_name} {sanitized_role}" 169 | team_slug = slugified(team_name) 170 | return team_slug, team_name 171 | 172 | 173 | def pluralized(word): 174 | """ 175 | Get the plural of the given word. Contains a dictionary for non-standard 176 | plural forms. If the word ends in one of 5 known endings, appends an 'es' 177 | to the end. By default, just appends an 's' to the given word. 178 | 179 | @param word: the word to pluralize 180 | @return: the plural form of the noun 181 | """ 182 | defined_plurals = {"person": "people"} 183 | if word in defined_plurals: 184 | return defined_plurals[word] 185 | 186 | es_endings = ["s", "sh", "ch", "x", "z"] 187 | if any([word.endswith(ending) for ending in es_endings]): 188 | return f"{word}es" 189 | else: 190 | return f"{word}s" 191 | 192 | 193 | def slugified(text): 194 | """ 195 | Get the slug generated from the given text. Replaces all non-alphanumeric 196 | characters with hyphens. Coalesces successive hyphens into one. 197 | 198 | @param text: the text to slugify 199 | @return: the slug made from the given text 200 | """ 201 | return re.sub("-+", "-", re.sub(r"\W", "-", text.lower())) 202 | -------------------------------------------------------------------------------- /ccos/log.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import inspect 3 | import logging 4 | 5 | SUCCESS = logging.INFO + 1 6 | 7 | 8 | class IndentFormatter(logging.Formatter): 9 | """ 10 | Format the given log messages with proper indentation based on the stack 11 | depth of the code invoking the logger. This removes the need for manual 12 | indentation using tab characters. 13 | """ 14 | 15 | # https://en.wikipedia.org/wiki/ANSI_escape_code 16 | color_map = { # ............. Level ## Color ## 17 | logging.CRITICAL: 31, # . CRITICAL 50 red 31 18 | logging.ERROR: 31, # .... ERROR 40 red 31 19 | logging.WARNING: 33, # .. WARNING 30 yellow 33 20 | SUCCESS: 32, # .......... SUCCESS 21 green 32 21 | logging.INFO: 34, # ..... INFO 20 blue 34 22 | logging.DEBUG: 35, # .... DEBUG 10 magenta 35 23 | } 24 | 25 | @staticmethod 26 | def identify_cut(filenames): 27 | """ 28 | Identify the depth at which the invoking function can be located. The 29 | invoking function would be the first occurrence of a file just after 30 | all stack filenames from within Python libs itself. 31 | @param filenames: the names of all files from which logs were pushed 32 | @return: the index of the filename from which the logger was called 33 | """ 34 | lib_string = "lib/python" 35 | lib_started = False 36 | for index, filename in enumerate(filenames): 37 | if not lib_started and lib_string in filename: 38 | lib_started = True 39 | if lib_started and lib_string not in filename: 40 | return index 41 | 42 | def __init__(self): 43 | """ 44 | Initialise the formatter with the fixed log format. The format is 45 | intentionally minimal to get clean and readable logs. 46 | """ 47 | fmt = "%(message)s" 48 | super().__init__(fmt=fmt) 49 | 50 | self.baseline = None 51 | self.cut = None 52 | self.manual_push = 0 53 | 54 | def update_format(self, record): 55 | """ 56 | Update the format string based on the log level of the record. 57 | @param record: the record based on whose level to update the formatting 58 | """ 59 | prefix = "\u001b[" 60 | color = f"{prefix}{self.color_map[record.levelno]}m" 61 | bold = f"{prefix}1m" 62 | gray = f"{prefix}1m{prefix}30m" 63 | reset = f"{prefix}0m" 64 | self._style._fmt = ( 65 | f"%(asctime)s" 66 | f" {gray}│{reset} {color}%(levelname)-8s{reset} {gray}│{reset} " 67 | ) 68 | if hasattr(record, "function"): 69 | self._style._fmt += ( 70 | f"{gray}%(indent)s{reset}" 71 | f"{bold}%(function)s{reset}{gray}:{reset}" 72 | " %(message)s" 73 | ) 74 | else: 75 | self._style._fmt += "%(indent)s%(message)s" 76 | 77 | def format(self, record): 78 | """ 79 | Format the log message with additional data extracted from the stack. 80 | @param record: the log record to format with this formatter 81 | @return: the formatted log record 82 | """ 83 | stack = inspect.stack(context=0) 84 | depth = len(stack) 85 | if self.baseline is None: 86 | self.baseline = depth 87 | if self.cut is None: 88 | filenames = map(lambda x: x.filename, stack) 89 | self.cut = self.identify_cut(filenames) 90 | 91 | # Inject custom information into the record 92 | record.indent = "." * (depth - self.baseline + self.manual_push) 93 | if depth > self.cut: 94 | record.function = stack[self.cut].function 95 | 96 | # Format the record using custom information 97 | self.update_format(record) 98 | out = super().format(record) 99 | 100 | # Remove custom information from the record 101 | del record.indent 102 | if hasattr(record, "function"): 103 | del record.function 104 | 105 | return out 106 | 107 | def delta_indent(self, delta=1): 108 | """ 109 | Change the manual push value by the given number of steps. Increasing 110 | the value indents the logs and decreasing it de-indents them. 111 | @param delta: the number of steps by which to indent/de-indent the logs 112 | """ 113 | self.manual_push += delta 114 | 115 | def reset(self): 116 | """ 117 | Reset the baseline and cut attributes so that the next call to the 118 | logger can repopulate them to the new values for the particular file. 119 | """ 120 | self.baseline = None 121 | self.cut = None 122 | self.manual_push = 0 123 | 124 | 125 | def setup_logger(): 126 | """ 127 | Configure RootLogger. This method must be called only once from the main 128 | script (not from modules/libraries included by that script). 129 | """ 130 | 131 | def log_success_class(self, message, *args, **kwargs): 132 | if self.isEnabledFor(SUCCESS): 133 | # The 'args' below (instead of '*args') is correct 134 | self._log(SUCCESS, message, args, **kwargs) 135 | 136 | def log_success_root(message, *args, **kwargs): 137 | logging.log(SUCCESS, message, *args, **kwargs) 138 | 139 | def change_indent_class(self, delta=1): 140 | """ 141 | Indent the output of the logger by the given number of steps. If 142 | positive, the indentation increases and if negative, it decreases. 143 | @param delta: the number of steps by which to indent/de-indent the logs 144 | """ 145 | handlers = self.handlers 146 | if len(handlers) > 0: 147 | formatter = handlers[-1].formatter 148 | if isinstance(formatter, IndentFormatter): 149 | formatter.delta_indent(delta) 150 | 151 | logging.addLevelName(SUCCESS, "SUCCESS") 152 | setattr(logging.getLoggerClass(), "success", log_success_class) 153 | setattr(logging, "success", log_success_root) 154 | setattr(logging.getLoggerClass(), "change_indent", change_indent_class) 155 | 156 | formatter = IndentFormatter() 157 | 158 | handler = logging.StreamHandler() 159 | handler.setFormatter(formatter) 160 | 161 | logger = logging.root 162 | logger.addHandler(handler) 163 | logger.setLevel(logging.INFO) 164 | 165 | return logger 166 | 167 | 168 | __all__ = ["setup_logger"] 169 | -------------------------------------------------------------------------------- /ccos/manage/projects.yml: -------------------------------------------------------------------------------- 1 | --- 2 | possumbilities: # Web Development and Web Support 3 | repos: 4 | - attribution-license-plugin 5 | - australian-chapter 6 | - business-toolkit-files 7 | - candela-utility 8 | - cc-accidenz-commons 9 | - cc-assets 10 | - cc-license-chooser 11 | - cc-resource-archive 12 | - cc-usability-prototypes 13 | - certificates-landing-page-theme 14 | - chapters 15 | - chooser 16 | - commoners 17 | - Creative-Commons-Post-Republisher 18 | - creativecommons-base 19 | - creativecommons-certificate 20 | - global-network-strategy 21 | - index-dev-env 22 | - index-prototype 23 | - network-platforms 24 | - og-image-generator 25 | - open4us.org 26 | - queulat 27 | - reversionary-rights 28 | - search 29 | - taaccct 30 | - team-open 31 | - termination-of-transfer 32 | - the-power-of-open 33 | - vocabulary 34 | - vocabulary-theme 35 | - wp-plugin-cc-global-network 36 | - wp-plugin-creativecommons-website 37 | - wp-theme-cc-chapter 38 | - wp-theme-cc-commoners 39 | - wp-theme-cctoolkit 40 | - wp-theme-creativecommons.org 41 | - wp-theme-openglam 42 | - wp-theme-summit 43 | Shafiya-Heena: # IT Support, Platforms, and Systems 44 | repos: 45 | - ansible-dev 46 | - CCID-cas 47 | - CCID-MediaWiki 48 | - index-performance-testing 49 | - sre-salt 50 | - sre-salt-prime 51 | - sre-wiki-js 52 | - tech-support 53 | TimidRobot: # Application Programming and Management 54 | repos: 55 | - .github 56 | - cc-legal-tools-app 57 | - cc-legal-tools-data 58 | - ccos-scripts 59 | - ccrel 60 | - ccrel-guide 61 | - creativecommons.github.io 62 | - ccos-website-source 63 | - creativecommons.org 64 | - data-science-playground 65 | - eng-misc-scripts 66 | - faq 67 | - legaldb 68 | - licensebuttons 69 | - magical-pony 70 | - mp 71 | - quantifying 72 | - scholars-addenda 73 | - sre-report-to-wikijs 74 | - sre-wp-pull 75 | - stateof 76 | -------------------------------------------------------------------------------- /ccos/norm/branch_protections.yml: -------------------------------------------------------------------------------- 1 | # Specify repositories that are excluded from branch protections. 2 | # 3 | # Format: 4 | # # comment indicating reason for exclusion 5 | # - REPOSITORY 6 | EXEMPT_REPOSITORIES: 7 | # special purpose repo 8 | - australian-chapter 9 | # exempted for bot pushes to default branch 10 | - ccos-website-source 11 | # exempted for bot pushes to default branch 12 | - creativecommons.github.io 13 | # special purpose repo 14 | - global-network-strategy 15 | # special purpose repo 16 | - network-platforms 17 | # special purpose repo 18 | - sre-wiki-js 19 | # special purpose repo 20 | - tech-support 21 | 22 | # Specify non-exempt repositories requiring specific status checks 23 | # 24 | # Format: 25 | # REPOSITORY: 26 | # - STATUS_CHECK_NAME 27 | REQUIRED_STATUS_CHECK_MAP: 28 | ccos-website-source: 29 | - Build and Deploy CC Open Source 30 | 31 | # Specify non-exempt repositories and the people, teams, or apps who are 32 | # allowed to bypass required pull requests (PRs) 33 | # 34 | # Format: 35 | # REPOSITORY: 36 | # - GITHUB_USER_TEAM_OR_APP 37 | # - GITHUB_USER_TEAM_OR_APP 38 | EXEMPT_USERS: 39 | quantifying: 40 | - cc-quantifying-bot 41 | -------------------------------------------------------------------------------- /ccos/norm/get_labels.py: -------------------------------------------------------------------------------- 1 | # Standard library 2 | import logging 3 | from pathlib import Path 4 | 5 | # Third-party 6 | import yaml 7 | 8 | # First-party/Local 9 | from ccos.norm.models import Group, Label 10 | 11 | LOG = logging.root 12 | 13 | 14 | def get_required_label_groups(): 15 | """ 16 | Get the list of all the label_groups, at least one label of which is 17 | required to be present on every triaged issue. 18 | @return: the filtered list of label groups that that are required by 19 | definition 20 | """ 21 | labels_dict = load_yaml_from_file("labels") 22 | label_groups = [] 23 | for group_info in labels_dict["groups"]: 24 | group = Group(**group_info) 25 | label_names = group_info.pop("labels", []) 26 | label_groups.append(group) 27 | for label_info in label_names: 28 | Label(**label_info, group=group) 29 | 30 | LOG.info(f"Filtering {len(label_groups)} label_groups...") 31 | required_label_groups = [ 32 | group for group in label_groups if group.is_required 33 | ] 34 | LOG.success(f"done. Required {len(required_label_groups)} label groups.") 35 | return required_label_groups 36 | 37 | 38 | def get_standard_labels(): 39 | """ 40 | Get the list of standard labels that apply to every repository. 41 | @return: the list of standard labels 42 | """ 43 | labels_dict = load_yaml_from_file("labels") 44 | standard_labels = [] 45 | for group_info in labels_dict["groups"]: 46 | label_names = group_info.pop("labels", []) 47 | group = Group(**group_info) 48 | for label_info in label_names: 49 | label = Label(**label_info, group=group) 50 | standard_labels.append(label) 51 | for label_info in labels_dict["standalone"]: 52 | label = Label(**label_info) 53 | standard_labels.append(label) 54 | return standard_labels 55 | 56 | 57 | def get_repo_specific_labels(): 58 | """ 59 | Get the dict mapping each repository to a list of skill labels. For each 60 | repository, skill levels will be added on top of the standard labels. 61 | @return: the dict mapping repo names to skill labels 62 | """ 63 | skill_group = Group(color="5ff1f5", name="skill") 64 | labels_dict = load_yaml_from_file("skills") 65 | repo_specific_labels = {} 66 | for repo_name, skill_names in labels_dict.items(): 67 | skill_labels = [ 68 | get_skill_label_from_name(skill_group, skill_name) 69 | for skill_name in skill_names 70 | ] 71 | repo_specific_labels[repo_name] = skill_labels 72 | return repo_specific_labels 73 | 74 | 75 | def get_skill_label_from_name(skill_group, skill_name): 76 | """ 77 | Generate the skill label purely from the name of the skill. The name of the 78 | skill is plugged into the description and the lower-cased version is used 79 | as the label name. 80 | @param skill_group: the logical parent group of all skill labels 81 | @param skill_name: the name of the skill to convert into a label 82 | @return: an instance of Label derived from the skill name 83 | """ 84 | return Label( 85 | name=skill_name.lower(), 86 | description=f"Requires proficiency in '{skill_name}'", 87 | emoji="💪", 88 | group=skill_group, 89 | ) 90 | 91 | 92 | def load_yaml_from_file(file_name): 93 | """ 94 | Load the YAML file into a Python list or dict. The extension '.yml' is 95 | appended to the file name and only the current directory is scanned for the 96 | matching file. 97 | @param file_name: the name of the file to load 98 | @return: the contents of the YAML file as a Python object 99 | """ 100 | file_path = get_datafile_path(file_name) 101 | with open(file_path, "r") as file: 102 | data = yaml.safe_load(file) 103 | return data 104 | 105 | 106 | def get_datafile_path(file_name): 107 | """ 108 | Get the path to the datafile by searching the current directory for a file 109 | with the given name and the '.yml' extension. 110 | @param file_name: the name of the file whose path is required 111 | @return: the path to the file 112 | """ 113 | current_file = Path(__file__).resolve() 114 | data_file = current_file.parent.joinpath(f"{file_name}.yml") 115 | return data_file 116 | 117 | 118 | def get_labels(): 119 | """ 120 | Get the list of standard and repository-specific labels. 121 | @return: the list of standard and repository-specific labels 122 | """ 123 | standard_labels = get_standard_labels() 124 | repo_specific_labels = get_repo_specific_labels() 125 | return standard_labels, repo_specific_labels 126 | 127 | 128 | __all__ = ["get_labels", "get_required_label_groups"] 129 | -------------------------------------------------------------------------------- /ccos/norm/labels.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: priority 3 | is_required: true 4 | labels: 5 | - name: critical 6 | color: 'UNFAVOURABLE' 7 | description: Must be fixed ASAP 8 | emoji: "🟥" 9 | - name: high 10 | color: 'NEGATIVE' 11 | description: Stalls work on the project or its dependents 12 | emoji: "🟧" 13 | - name: medium 14 | color: 'NEUTRAL' 15 | description: Not blocking but should be fixed soon 16 | emoji: "🟨" 17 | - name: low 18 | color: 'POSITIVE' 19 | description: Low priority and doesn't need to be rushed 20 | emoji: "🟩" 21 | 22 | - name: status 23 | is_required: true 24 | labels: 25 | - name: awaiting triage 26 | color: 'DARKER' 27 | description: Has not been triaged & therefore, not ready for work 28 | emoji: "🚦" 29 | - name: blocked 30 | color: 'MEDIUM' 31 | description: Blocked & therefore, not ready for work 32 | emoji: "🚧" 33 | - name: discarded 34 | color: 'LIGHTER' 35 | description: Will not be worked on 36 | emoji: "⛔️" 37 | - name: discontinued 38 | color: 'LIGHTER' 39 | description: Not suitable for work as repo is in maintenance 40 | emoji: "🙅" 41 | - name: ready for work 42 | color: 'LIGHT' 43 | description: Ready for work 44 | emoji: "🏁" 45 | - name: label work required 46 | color: 'DARK' 47 | description: Needs proper labelling before it can be worked on 48 | emoji: "🏷" 49 | - name: ticket work required 50 | color: 'DARK' 51 | description: Needs more details before it can be worked on 52 | emoji: "🧹" 53 | 54 | - name: goal 55 | color: 'ffffff' 56 | is_required: true 57 | labels: 58 | - name: addition 59 | description: Addition of new feature 60 | emoji: "🌟" 61 | - name: fix 62 | description: Bug fix 63 | emoji: "🛠" 64 | - name: improvement 65 | description: Improvement to an existing feature 66 | emoji: "✨" 67 | 68 | - name: aspect 69 | color: '04338c' 70 | is_required: true 71 | labels: 72 | - name: code 73 | description: Concerns the software code in the repository 74 | emoji: "💻" 75 | - name: docs 76 | description: Concerns the documentation in the repository 77 | emoji: "📖" 78 | - name: dx 79 | description: Concerns developers' experience with the codebase 80 | emoji: "🤖" 81 | - name: interface 82 | description: Concerns end-users' experience with the software 83 | emoji: "🕹" 84 | - name: text 85 | description: Concerns the textual material in the repository 86 | emoji: "📄" 87 | 88 | - name: talk 89 | color: 'f9bbe5' 90 | labels: 91 | - name: discussion 92 | description: Open for discussions and feedback 93 | emoji: "💬" 94 | - name: question 95 | description: Can be resolved with an answer 96 | emoji: "❓" 97 | 98 | - name: friendliness 99 | color: '7f0799' 100 | is_prefixed: false 101 | labels: 102 | - name: good first issue 103 | description: New-contributor friendly 104 | emoji: "🤗" 105 | has_emoji_name: false 106 | - name: help wanted 107 | description: Open to participation from the community 108 | emoji: "🙏" 109 | has_emoji_name: false 110 | - name: staff only 111 | description: Restricted to CC staff members 112 | emoji: "🔒" 113 | 114 | standalone: 115 | - name: 'ノಠ益ಠノ彡┻━┻' 116 | color: '000000' 117 | description: Aaargh! 118 | emoji: "🤯" 119 | 120 | - name: invalid 121 | color: 'LIGHTER' 122 | description: Inappropriate or invalid (ex. Hacktoberfest spam) 123 | emoji: "⛔️" 124 | has_emoji_name: false 125 | -------------------------------------------------------------------------------- /ccos/norm/models.py: -------------------------------------------------------------------------------- 1 | COLORS = { 2 | "UNFAVOURABLE": "b60205", 3 | "NEGATIVE": "ff9f1c", 4 | "NEUTRAL": "ffcc00", 5 | "POSITIVE": "cfda2c", 6 | "FAVOURABLE": "008672", 7 | "BLACK": "000000", 8 | "DARKER": "333333", 9 | "DARK": "666666", 10 | "MEDIUM": "999999", 11 | "LIGHT": "cccccc", 12 | "LIGHTER": "eeeeee", 13 | "WHITE": "ffffff", 14 | } 15 | 16 | 17 | class Group: 18 | """ 19 | This model represents a group of labels. A group has some fixed parameters 20 | - name, which may be prefixed to all child label names 21 | - color, which acts as a fallback for child labels that do not specify one 22 | - is_prefixed, which determines if group name is prefixed on child labels 23 | - is_required, which determines if >=1 sub-label must be applied on issues 24 | """ 25 | 26 | def __init__( 27 | self, color=None, is_prefixed=True, is_required=False, **kwargs 28 | ): 29 | self.name = kwargs["name"] 30 | self.color = color 31 | self.is_prefixed = is_prefixed 32 | self.is_required = is_required 33 | 34 | self.labels = [] # This may or may not be populated, do not rely 35 | 36 | def __str__(self): 37 | return self.name 38 | 39 | def __repr__(self): 40 | return f"" 41 | 42 | 43 | class Label: 44 | """ 45 | This model represents a single label. A label is defined by four parameters 46 | - name, which appears on the label 47 | - description, which describes it in a little more detail 48 | - emoji, which is a pictorial representation of the purpose of the label 49 | - color, which is used as a background on the label element 50 | 51 | A ``Label`` instance is associated to a ``Group`` instance by a many to one 52 | relationship. 53 | """ 54 | 55 | def __init__(self, group=None, color=None, has_emoji_name=True, **kwargs): 56 | self.name = kwargs["name"] 57 | self.description = kwargs["description"] 58 | self.emoji = kwargs["emoji"] 59 | self.own_color = color 60 | self.has_emoji_name = has_emoji_name 61 | 62 | self.group = group 63 | if group and self not in group.labels: 64 | group.labels.append(self) 65 | 66 | @property 67 | def color(self): 68 | """ 69 | Return the color to use on the emoji label, given as a 6-digit 70 | hexadecimal code without the prefix '#'. Labels can have their color 71 | specified as a constant and if missing inherit color from the parent 72 | group. If not resolved, the color defaults to pure black. 73 | @return: the 6-digit hexadecimal code of the background color 74 | """ 75 | 76 | color = self.own_color 77 | if color is None and self.group is not None: 78 | color = self.group.color 79 | elif color is None: 80 | color = COLORS["BLACK"] 81 | elif color in COLORS: 82 | color = COLORS[color] 83 | return color 84 | 85 | @property 86 | def qualified_name(self): 87 | """ 88 | Return the fully qualified name of the label. Most label groups prefix 89 | the group name to the name of the label, separated by a dunder, as 90 | indicated by the ``is_prefixed`` attribute on the associated ``Group`` 91 | instance. 92 | @return: the fully qualified name of the label 93 | """ 94 | 95 | name = self.name 96 | if self.group and self.group.is_prefixed: 97 | name = f"{self.group}: {name}" 98 | if self.has_emoji_name: 99 | name = f"{self.emoji} {name}" 100 | return name 101 | 102 | @property 103 | def emojified_description(self): 104 | """ 105 | TODO: Use this when GitHub supports Unicode in label descriptions 106 | 107 | Get the description of the label prefixed with the emoji. 108 | @return: the emoji-prefixed description 109 | """ 110 | 111 | return f"{self.emoji} {self.description}" 112 | 113 | @property 114 | def api_arguments(self): 115 | """ 116 | Get the dictionary of arguments to pass to the API for creating the 117 | label. The API only accepts ``name``, ``color`` and ``description``. 118 | @return: the API arguments as a dictionary 119 | """ 120 | 121 | return { 122 | "name": self.qualified_name, 123 | "color": self.color, 124 | "description": self.description, 125 | } 126 | 127 | def __eq__(self, remote): 128 | """ 129 | Compare this instance with the corresponding PyGithub instance to 130 | determine whether the two are equal. 131 | @param remote: the PyGithub label instance to compare itself against 132 | @return: whether the instance is equal to its remote counterpart 133 | """ 134 | 135 | return all( 136 | [ 137 | self.qualified_name == remote.name, 138 | self.color == remote.color, 139 | self.description == remote.description, 140 | ] 141 | ) 142 | 143 | def __ne__(self, remote): 144 | """ 145 | Compare this instance with the corresponding PyGithub instance to 146 | determine whether the two are unequal and would need to be reconciled. 147 | @param remote: the PyGithub label instance to compare itself against 148 | @return: whether the instance is unequal to its remote counterpart 149 | """ 150 | 151 | return any( 152 | [ 153 | self.qualified_name != remote.name, 154 | self.color != remote.color, 155 | self.description != remote.description, 156 | ] 157 | ) 158 | 159 | def __str__(self): 160 | return self.qualified_name 161 | 162 | def __repr__(self): 163 | return f"