diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index c1ea231..81c76ee 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -1,12 +1,16 @@ -name: Update static API +name: Update static files on: repository_dispatch: workflow_dispatch: + inputs: + output-branch: + descripton: "Branch to push static files to" + required: true jobs: update: - name: Update static API + name: Update static files runs-on: ubuntu-latest steps: - name: Clone repository @@ -17,14 +21,14 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.10.10 + python-version: 3.11.3 - - name: Create configuration file + - name: Write config file run: echo "${{ vars.CONFIG }}" > config.json - - name: Check for existing deployment exists + - name: Check if a deployment exists run: | - if git ls-remote --exit-code --heads origin gh-pages; then + if git ls-remote --exit-code --heads origin ${{ inputs.output-branch }}; then echo "::set-output name=deployment-exists::true" else echo "::set-output name=deployment-exists::false" @@ -36,17 +40,17 @@ jobs: run: | git config user.name revanced-bot git config user.email github@revanced.app - git subtree add --prefix=static/ origin/gh-pages --squash + git subtree add --prefix=static/ origin/${{ inputs.output-branch }} --squash - - name: Update static API + - name: Update static files run: | - pip install requests - python src/main.py + poetry install + poetry run python main.py - name: Commit changes uses: EndBug/add-and-commit@v9 with: - message: "chore: updated static files" + message: "chore: Update static files" fetch: false push: false @@ -54,6 +58,6 @@ jobs: uses: s0/git-publish-subdir-action@develop env: REPO: self - BRANCH: gh-pages + BRANCH: ${{ inputs.output-branch }} FOLDER: static GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 6471f2a..91984e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ -config.json \ No newline at end of file +config.json +static \ No newline at end of file diff --git a/CNAME b/CNAME index 2de080c..9f1ffe6 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -static-api.osumatrix.me \ No newline at end of file +static-api.revanced.app \ No newline at end of file diff --git a/README.md b/README.md index 2a69f8b..4f95d9c 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,30 @@ # Static API -Proof of concept repository to host a static API using GitHub workflows. +Repository to host a static API using GitHub workflows. ## How it works -On CI trigger static files are generated and commited to the `gh-pages` branch. +When CI is ran, static files are generated and commited to the specified branch. +The file `generator.py` provides a list of static file generator classes. Each class has a name. +The configuration file `config.json` is read which contains the configuration for the API. +By specifying the name of the generator in the `generators` array of the configuration, the corresponding generator will be used. The current object of the configuration is passed to the generator. -The static files are generated by the configuration provided in a `config.json` file in the root. -The API configuration consists out of an array of objects. Each object is responsible for an API. Based on the provided pair with the key `type` in these objects a method in `generator.py` is selected to generate the API. The objects are passed to the corresponding method to consume additional key-value pairs. - -The following API configuration generates the `contributor` and `release` API for selected repositories: +The following configuration generates static files using the `contributors` and `releases` generator for selected repositories: ```json { "api": [ { - "type": "release+contributor", + "generators": ["releases", "contributors"], "repositories": ["user/repo"] } ] } ``` -All static files are generated in the output path specified in the configuration. +All static files are generated in the output path specified in the configuration. +The `purge` array in the configuration specifies which files should be deleted before generating the static files. ## Setup -A repository variable `CONFIG` is expected by CD with the configuration (string escaped) which will be used by CD to generate the static files. \ No newline at end of file +A repository variable `CONFIG` is expected by CD with the configuration (string escaped) which will be used by CD to generate the static files. diff --git a/src/app/__init__.py b/app/__init__.py similarity index 100% rename from src/app/__init__.py rename to app/__init__.py diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..e12e830 --- /dev/null +++ b/app/api.py @@ -0,0 +1,146 @@ +from abc import abstractmethod + +import requests + + +class Api: + _api_key: str | None + + @abstractmethod + def __init__(self, api_key: str | None = None): + self._api_key = api_key + + @abstractmethod + def get_release( + self, repository: str, all: bool = False, prerelease: bool = False + ) -> dict | list: + """Gets the release(s) for a repository. + + Args: + repository (str): The repository to get releases for. + all (bool, optional): Whether to get all releases or not. Defaults to False. + prerelease (bool, optional): Whether to get prereleases or not. Defaults to False. + Returns: + dict | list: The release(s) for the repository. + """ + raise NotImplementedError + + @abstractmethod + def get_contributor(self, repository): + """Gets the contributors for a repository. + + Args: + repository (str): The repository to get contributors for. + """ + raise NotImplementedError + + @abstractmethod + def get_members(self, organization): + '''Gets the team for an organization. + + Args: + organization (str): The organization to get the team for. + ''' + raise NotImplementedError + + +class GitHubApi(Api): + def __init__(self): + pass + + def get_contributor(self, repository): + def transform_contributor(contributor: dict) -> dict: + """Transforms a contributor into a dict. + + Args: + contributor (dict): The contributor to transform. + + Returns: + dict: The transformed contributor. + """ + + return { + "username": contributor["login"], + "avatar": contributor["avatar_url"], # TODO: Proxy via a CDN. + "link": contributor["html_url"], + "contributions": contributor["contributions"], + } + + def sort_and_delete_key(contributor: dict) -> int: + contributions = contributor["contributions"] + del contributor["contributions"] + return contributions + + contributors = requests.get( + f"https://api.github.com/repos/{repository}/contributors" + ).json() + contributors = list( + map(transform_contributor, contributors) + ) # List might not be needed. + contributors.sort(key=sort_and_delete_key, reverse=True) + + return contributors + + def get_release( + self, repository: str, all: bool = False, prerelease: bool = False + ) -> dict | list: + def transform_release(release: dict) -> dict: + """Transforms a release dict into a dict. + + Args: + release (dict): The release dict to transform. + + Returns: + dict: The transformed release dict. + """ + + return { + # TODO: Check if theres any need for this: 'id': release['id']. + "tag": release["tag_name"], + "prerelease": release["prerelease"], + "published_at": release["published_at"], + "assets": [ + { + "name": asset["name"], + "download_url": asset[ + "browser_download_url" + ], # TODO: Proxy via a CDN. + } + for asset in release["assets"] + ], + } + + # A little bit of code duplication but more readable than a ternary operation. + if all: + releases: list = requests.get( + f"https://api.github.com/repos/{repository}/releases" + ).json() + # List might not be needed. + return list(map(transform_release, releases)) + else: + latest_release: dict = requests.get( + f"https://api.github.com/repos/{repository}/releases/latest?prerelease={prerelease}" + ).json() + return transform_release(latest_release) + + def get_members(self, organization): + def transform_team_member(member: dict) -> dict: + '''Transforms a team member into a dict. + + Args: + member (dict): The member to transform. + + Returns: + dict: The transformed member. + ''' + + return { + 'username': member['login'], + 'avatar': member['avatar_url'], # TODO: Proxy via a CDN. + 'link': member['html_url'] + } + + members = requests.get( + f'https://api.github.com/orgs/{organization}/members').json() + # List might not be needed. + return list(map(transform_team_member, members)) diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..45b3e3e --- /dev/null +++ b/app/config.py @@ -0,0 +1,6 @@ +import json + + +def load_config() -> dict: + with open("config.json", "r") as config_file: + return json.load(config_file) diff --git a/app/generator.py b/app/generator.py new file mode 100644 index 0000000..5b718a6 --- /dev/null +++ b/app/generator.py @@ -0,0 +1,177 @@ +from os.path import join +from app import api +from app.utils import get_repository_name, to_json, write_json, read_json, create_if_not_exists +from abc import abstractmethod + +class Generator: + _api: api.Api + + def __init__(self, name: str, api: api.Api | None = None): + """ + Args: + name (str): The name of the generator + api (Api | None): An optional api to use for the generator. + """ + self.name = name + self._api = api + + @abstractmethod + def generate(self, config, path): + """ + Generates static files based on the supplied config to the specified path. + + Args: + config (dict): The configuration for the generator + path (str): The path to generate the static files to + """ + raise NotImplementedError + + +class ReleasesGenerator(Generator): + """ + Generates a release file for each repository in the config. + The release file is named after the tag of the release and contains the latest release information of the repository. + A `latest.json` file is also generated containing the latest release of the repository. + """ + + def __init__(self, api): + super().__init__("releases", api) + + def generate(self, config, path): + path = join(path, "releases") + + repositories = config["repositories"] + + for repository in repositories: + release = self._api.get_release(repository) + repository_name = get_repository_name(repository) + + tag = release["tag"] + + release_path = join(path, repository_name) + release_json = to_json(release) + + create_if_not_exists(release_path) + + write_json(release_json, join( + release_path, f"{tag}.json"), overwrite=False) + write_json( + release_json, join(release_path, "latest.json") + ) # Overwrite the latest release + + # At last join the current tag to an index file + index_path = join(path, f"{repository_name}.json") + + index = read_json(index_path, []) + if tag not in index: # TODO: Check if there a better way to do this + index.append(tag) # Add the current tag to the index + + write_json(index, index_path) + + +class ContributorsGenerator(Generator): + """ + Generates a contributor file for each repository in the config. + The contributor file is named after the repository and contains the contributors of the repository. + """ + + def __init__(self, api): + super().__init__("contributors", api) + + def generate(self, config, path): + path = join(path, "contributors") + + create_if_not_exists(path) + repositories = config["repositories"] + + for repository in repositories: + repository_name = get_repository_name(repository) + + contributors = self._api.get_contributor(repository) + contributors_path = join(path, f"{repository_name}.json") + + write_json(contributors, contributors_path) + + +class ConnectionsGenerator(Generator): + """ + Generates a file containing the connections of the organization. + """ + + def __init__(self, api): + super().__init__("connections", api) + + def generate(self, config, path): + new_connections = config["connections"] + + connections_path = join(path, f"connections.json") + + write_json(new_connections, connections_path) + + +class TeamGenerator(Generator): + """ + Generates a team file containing the members of the organization. + """ + + def __init__(self, api): + super().__init__("team", api) + + def generate(self, config, path): + organization = config["organization"] + + team = self._api.get_members(organization) + + team_path = join(path, f"team.json") + + write_json(team, team_path) + + +class DonationsGenerator(Generator): + """ + Generates a donation file containing ways to donate to the organization. + """ + + def __init__(self, api): + super().__init__("donations", api) + + def generate(self, config, path): + links = config["links"] if "links" in config else [] + wallets = config["wallets"] if "wallets" in config else [] + + donation_path = join(path, f"donations.json") + + write_json( + { + "links": links, + "wallets": wallets + }, + donation_path + ) + + +class GeneratorProvider: + generators: list[Generator] + + def __init__(self, generators: list[Generator]): + self.generators = generators + + def get(self, name: str) -> Generator | None: + for generator in self.generators: + if generator.name == name: + return generator + + return None + + +class DefaultGeneratorProvider(GeneratorProvider): + def __init__(self): + self._api = api.GitHubApi() + + super().__init__([ + ReleasesGenerator(self._api), + ContributorsGenerator(self._api), + ConnectionsGenerator(self._api), + TeamGenerator(self._api), + DonationsGenerator(self._api) + ]) diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..5b58bdc --- /dev/null +++ b/app/utils.py @@ -0,0 +1,28 @@ +import json +import os + + +def write_json(text: str | dict | list, to, overwrite=True): + if not os.path.exists(to) or overwrite: + with open(to, "w") as f: + text = to_json(text) + f.write(text) + +def to_json(text: str | dict | list): + if not isinstance(text, str): + text = json.dumps(text, indent=2) + return text + +def read_json(path, default): + if os.path.exists(path): + with open(path, "r") as f: + return json.load(f) + return default + + +def create_if_not_exists(path): + os.makedirs(path, exist_ok=True) + + +def get_repository_name(repository: str): + return repository.split("/")[-1] diff --git a/config.example.json b/config.example.json index bee0bad..63953cf 100644 --- a/config.example.json +++ b/config.example.json @@ -1,17 +1,114 @@ { - "api": [ + "configs": [ { - "type": "release+contributor", + "generators": [ + "releases", + "contributors" + ], "repositories": [ - "user/repo" + "revanced/revanced-patches", + "revanced/revanced-integrations", + "revanced/revanced-manager" ] }, { - "type": "social", - "socials": { - "website": "https://yourwebsite.com" - } + "generators": [ + "connections" + ], + "connections": [ + { + "name": "Website", + "url": "https://revanced.app", + "preferred": true + }, + { + "name": "GitHub", + "url": "https://github.com/revanced", + "preferred": false + }, + { + "name": "Twitter", + "url": "https://twitter.com/revancedapp", + "preferred": false + }, + { + "name": "Discord", + "url": "https://revanced.app/discord", + "preferred": true + }, + { + "name": "Reddit", + "url": "https://www.reddit.com/r/revancedapp", + "preferred": false + }, + { + "name": "Telegram", + "url": "https://t.me/app_revanced", + "preferred": false + }, + { + "name": "YouTube", + "url": "https://www.youtube.com/@ReVanced", + "preferred": false + } + ] + }, + { + "generators": [ + "team" + ], + "organization": "revanced" + }, + { + "generators": [ + "donations" + ], + "links": [ + { + "name": "Open Collective", + "url": "https://opencollective.com/revanced", + "preferred": true + }, + { + "name": "GitHub Sponsors", + "url": "https://github.com/sponsors/ReVanced", + "preferred": false + } + ], + "wallets": [ + { + "network": "Bitcoin", + "currency_code": "BTC", + "address": "bc1q4x8j6mt27y5gv0q625t8wkr87ruy8fprpy4v3f", + "preferred": false + }, + { + "network": "Dogecoin", + "currency_code": "DOGE", + "address": "D8GH73rNjudgi6bS2krrXWEsU9KShedLXp", + "preferred": true + }, + { + "network": "Ethereum", + "currency_code": "ETH", + "address": "0x7ab4091e00363654bf84B34151225742cd92FCE5", + "preferred": false + }, + { + "network": "Litecoin", + "currency_code": "LTC", + "address": "LbJi8EuoDcwaZvykcKmcrM74jpjde23qJ2", + "preferred": false + }, + { + "network": "Monero", + "currency_code": "XMR", + "address": "46YwWDbZD6jVptuk5mLHsuAmh1BnUMSjSNYacozQQEraWSQ93nb2yYVRHoMR6PmFYWEHsLHg9tr1cH5M8Rtn7YaaGQPCjSh", + "preferred": false + } + ] } ], - "output": "static" + "output": "static", + "purge": [] } \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ad0d711 --- /dev/null +++ b/main.py @@ -0,0 +1,26 @@ +from genericpath import isdir, isfile +import os +import shutil +from app.config import load_config +from app.generator import DefaultGeneratorProvider + +config = load_config() + +output = config["output"] if "output" in config else "static" +purge = config["purge"] if "purge" in config else [] +generator_configs = config["configs"] + +generator_provider = DefaultGeneratorProvider() + +for path in purge: + if isdir(path): + shutil.rmtree(path) + elif isfile(path): + os.remove(path) + +for config in generator_configs: + for generator_name in config["generators"]: + generator = generator_provider.get(generator_name) + if generator is None: + continue + generator.generate(config, output) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..2cccf11 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,179 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, + {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, +] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "types-requests" +version = "2.31.0.10" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "types-requests-2.31.0.10.tar.gz", hash = "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"}, + {file = "types_requests-2.31.0.10-py3-none-any.whl", hash = "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "747d86bc4792bf2d1f4713f4ee23731066fd73ef95c3bb6a15bbdce896232193" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..acd2622 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "revanced-static-api" +version = "0.1.0" +description = "Static API for ReVanced" +authors = ["Alexandre Teles ", "oSumAtrIX "] +license = "GPLv3" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +requests = "^2.28.2" + +[tool.poetry.group.dev.dependencies] +types-requests = "^2.28.11.17" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/src/app/api.py b/src/app/api.py deleted file mode 100644 index 9a61595..0000000 --- a/src/app/api.py +++ /dev/null @@ -1,97 +0,0 @@ -from abc import abstractmethod - -import requests - -class Api(): - _api_key: str - - @abstractmethod - def __init__(self, api_key: str = None) -> None: - self._api_key: str = api_key - - @abstractmethod - def get_release(self, repository: str, all: bool = False, prerelease: bool = False) -> dict | list: - '''Gets the release(s) for a repository. - - Args: - repository (str): The repository to get releases for. - all (bool, optional): Whether to get all releases or not. Defaults to False. - prerelease (bool, optional): Whether to get prereleases or not. Defaults to False. - Returns: - dict | list: The release(s) for the repository. - ''' - raise NotImplementedError - - @abstractmethod - def get_contributor(self, repository): - '''Gets the contributors for a repository. - - Args: - repository (str): The repository to get contributors for. - ''' - raise NotImplementedError - -class GitHubApi(Api): - def __init__(self) -> None: - pass - - def get_contributor(self, repository): - def transform_contributor(contributor: dict) -> dict: - '''Transforms a contributor into a dict. - - Args: - contributor (dict): The contributor to transform. - - Returns: - dict: The transformed contributor. - ''' - - return { - 'username': contributor['login'], - 'avatar': contributor['avatar_url'], # TODO: Proxy via a CDN. - 'link': contributor['html_url'], - 'contributions': contributor['contributions'] - } - - def sort_and_delete_key(contributor: dict) -> int: - contributions = contributor['contributions'] - del contributor['contributions'] - return contributions - - contributors = requests.get(f'https://api.github.com/repos/{repository}/contributors').json() - contributors = list(map(transform_contributor, contributors)) # List might not be needed. - contributors.sort(key=sort_and_delete_key, reverse=True) - - return contributors - - def get_release(self, repository: str, all: bool = False, prerelease: bool = False) -> dict | list: - def transform_release(release: dict) -> dict: - '''Transforms a release dict into a dict. - - Args: - release (dict): The release dict to transform. - - Returns: - dict: The transformed release dict. - ''' - - return { - # TODO: Check if theres any need for this: 'id': release['id']. - 'tag': release['tag_name'], - 'prerelease': release['prerelease'], - 'published_at': release['published_at'], - 'assets': [ - { - 'name': asset['name'], - 'download_url': asset['browser_download_url'] # TODO: Proxy via a CDN. - } for asset in release['assets'] - ] - } - - # A little bit of code duplication but more readable than a ternary operation. - if all: - releases: list = requests.get(f'https://api.github.com/repos/{repository}/releases').json() - return list(map(transform_release, releases)) # List might not be needed. - else: - latest_release: object = requests.get(f'https://api.github.com/repos/{repository}/releases/latest?prerelease={prerelease}').json() - return transform_release(latest_release) diff --git a/src/app/config.py b/src/app/config.py deleted file mode 100644 index 7256ee4..0000000 --- a/src/app/config.py +++ /dev/null @@ -1,5 +0,0 @@ -import json - -def load_config() -> dict: - with open('config.json', 'r') as config_file: - return json.load(config_file) diff --git a/src/app/generator.py b/src/app/generator.py deleted file mode 100644 index 9d20905..0000000 --- a/src/app/generator.py +++ /dev/null @@ -1,110 +0,0 @@ -import json -from os.path import join -from app import api -from app.utils import get_repository_name, write_json, read_json, create_if_not_exists -from abc import abstractmethod - -class Api(): - _api: api.Api - - def __init__(self, name: str, api: api.Api = api.GitHubApi()) -> None: - self.name = name - self._api = api - - @abstractmethod - def generate(self, config, path): - ''' - Generates the api based on the config to the path. - - Args: - config (dict): The config for the api - path (str): The path where the api should be generated - ''' - raise NotImplementedError - -class ReleaseApi(Api): - def __init__(self, api) -> None: - super().__init__("release", api) - pass - - def generate(self, config, path): - path = join(path, 'release') - - repositories = config["repositories"] - - for repository in repositories: - release = self._api.get_release(repository) - repository_name = get_repository_name(repository) - - tag = release['tag'] - - release_path = join(path, repository_name) - release_json = json.dumps(release) - - create_if_not_exists(release_path) - - write_json(release_json, join(release_path, f'{tag}.json'), overwrite=False) - write_json(release_json, join(release_path, 'latest.json')) # Overwrite the latest release - - # At last join the current tag to an index file - index_path = join(path, f'{repository_name}.json') - - index = read_json(index_path, []) - if tag not in index: # TODO: Check if there a better way to do this - index.append(tag) # Add the current tag to the index - - write_json(index, index_path) - -class ContributorApi(Api): - def __init__(self, api) -> None: - super().__init__("contributor", api) - pass - - def generate(self, config, path): - path = join(path, 'contributor') - - create_if_not_exists(path) - repositories = config["repositories"] - - for repository in repositories: - repository_name = get_repository_name(repository) - - contributors = self._api.get_contributor(repository) - contributors_path = join(path, f'{repository_name}.json') - - write_json(contributors, contributors_path) - -class SocialApi(Api): - def __init__(self, api) -> None: - super().__init__("social", api) - - def generate(self, config, path): - new_social = config - - social_path = join(path, f"social.json") - social = read_json(social_path, new_social) - - write_json(social, social_path) - -class ApiProvider(): - _apis: list[Api] - - def __init__(self, apis: list[Api]) -> None: - self._apis = apis - - def get(self, name: str) -> Api: - for api in self._apis: - if api.name == name: - return api - - return None - -class DefaultApiProvider(ApiProvider): - def __init__(self): - self._api = api.GitHubApi() # Use GitHub as default api - - super().__init__([ - ReleaseApi(self._api), - ContributorApi(self._api), - SocialApi(self._api)] - ) diff --git a/src/app/utils.py b/src/app/utils.py deleted file mode 100644 index 643bdf2..0000000 --- a/src/app/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -import json -import os - -def write_json(text: str | dict | list, to, overwrite=True): - if not os.path.exists(to) or overwrite: - with open(to, 'w') as f: - if not isinstance(text, str): - text = json.dumps(text) - f.write(text) - -def read_json(path, default): - if os.path.exists(path): - with open(path, 'r') as f: - return json.load(f) - return default - -def create_if_not_exists(path): - os.makedirs(path, exist_ok=True) - -def get_repository_name(repository: str): - return repository.split('/')[-1] \ No newline at end of file diff --git a/src/main.py b/src/main.py deleted file mode 100644 index cb3d2df..0000000 --- a/src/main.py +++ /dev/null @@ -1,17 +0,0 @@ -from app.config import load_config -from app.generator import DefaultApiProvider - -config = load_config() - -output = config['output'] -apis = config['api'] - -api_provider = DefaultApiProvider() - -for api in apis: - types = api['type'].split('+') - del api['type'] # Don't need the type for the api anymore below - for type in types: - api_type = api_provider.get(type) - if api_type is None: continue - api_type.generate(api, output) \ No newline at end of file