[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".github/workflows/backend.yaml",
    "content": "name: CI-Backend\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n      - name: Set up Python 3.11\n        uses: actions/setup-python@v2\n        with:\n          python-version: 3.11\n      - name: Install dependencies\n        run: |\n          cd backend\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n      - name: Test with unittest\n        run: |\n          cd backend\n          python -m unittest\n        env:\n          AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }}\n          MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }}\n      - name: Upload coverage to Coveralls\n        run: |\n          cd backend\n          coverage run --source=src -m unittest\n          coveralls\n        env:\n          AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }}\n          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}\n          MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }}\n"
  },
  {
    "path": ".github/workflows/frontend.yaml",
    "content": "name: CI-Frontend\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - name: Install modules\n      run: |\n        cd frontend\n        yarn\n    - name: Run ESLint\n      run: |\n        cd frontend\n        yarn eslint ."
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n__pycache__\n\n.vscode\n\nbackend/.env\nbackend/.venv\nbackend/.coverage\nbackend/gcloud_key.json\n\nfrontend/.env\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Abhijit Gupta\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# GitHub Trends\n\n## SPECIAL: GitHub Wrapped\n\nCheck out your GitHub Wrapped at `githubwrapped.io`!\n\n![github-wrapped](https://github.com/avgupta456/github-trends/assets/16708871/bf9406a4-6a49-4dbf-8f60-af221bb84bd6)\n\n---\n\n## What is GitHub Trends\n\nGitHub Trends dives deep into the GitHub API to bring you exciting and impactful metrics about your code contributions. Generate insights on lines written by language, repository, and time. Easily embed dynamic images into your GitHub profile to share your statistics with the world. Check out some of the examples below:\n\n<a href=\"https://githubtrends.io\">\n  <img align=\"center\" src=\"https://api.githubtrends.io/user/svg/avgupta456/langs?time_range=one_year&include_private=True&loc_metric=changed\" />\n</a>\n<a href=\"https://githubtrends.io\">\n  <img align=\"center\" src=\"https://api.githubtrends.io/user/svg/avgupta456/repos?time_range=one_year&include_private=True&group=private&loc_metric=changed\" />\n</a>\n\n## Quickstart\n\nFirst, visit `https://api.githubtrends.io/auth/signup/public` and create an account with GitHub Trends. Then, paste this string into your Markdown content, substituting your username.\n\n```md\n[![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs)](https://githubtrends.io)\n```\n\nAnd voila, you get a card like above. Keep reading to learn more!\n\n## Why GitHub Trends?\n\nUnlike other projects which look at just your public repositories, GitHub Trends computes metrics based on your individual commits. If you commit to open-source projects, or have collaborators contribute to your own repositories, GitHub Trends will better measure your own code contributions. Through this method, GitHub Trends is the first project that allows users to surface lines of code written (LOC) by language and repository. Our web interface also allows for easier customization.\n\n# Usage\n\n## Website Workflow (Alpha)\n\nVisit [githubtrends.io](https://www.githubtrends.io) to create an account and get started!\n\nHave questions? Check out [the demo](https://www.githubtrends.io/demo)!\n\n![image](https://user-images.githubusercontent.com/16708871/138611082-105e4dbc-8a27-4f68-8045-f9d86c912429.png)\n\n---\n\n## API Workflow (Alpha)\n\nAlternatively, you can communicate directly with the API to create and customize your cards. Read [docs/API.md](https://github.com/avgupta456/github-trends/blob/main/docs/API.md) to learn more about the API and customizations.\n\n## FAQ\n\nSee [docs/FAQ.md](https://github.com/avgupta456/github-trends/blob/main/docs/FAQ.md).\n\n## Contributing\n\nSee [docs/CONTRIBUTING.md](https://github.com/avgupta456/github-trends/blob/main/docs/CONTRIBUTING.md).\n\n## Acknowledgements\n\nMuch inspiration was taken from [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats). If you haven't already, check it out and give it a star!\n"
  },
  {
    "path": "backend/.coveragerc",
    "content": "[run]\nsource = src\n\nomit =\n    ./.venv/*\n    ./tests/*\n    ./models/*\n    */__init__.py\n\n[report]\nomit =\n    ./.venv/*\n    ./tests/*\n    ./models/*\n    */__init__.py"
  },
  {
    "path": "backend/.env-template",
    "content": "AUTH_TOKEN=abc123\nCOVERALLS_REPO_TOKEN=abc123\n\nPROD_OAUTH_CLIENT_ID=abc123\nPROD_OAUTH_CLIENT_SECRET=abc123\nPROD_OAUTH_REDIRECT_URI=abc123\n\nDEV_OAUTH_CLIENT_ID=abc123\nDEV_OAUTH_CLIENT_SECRET=abc123\nDEV_OAUTH_REDIRECT_URI=abc123\n\nGOOGLE_APPLICATION_CREDENTIALS=abc123\n\nMONGODB_PASSWORD=abc123\n"
  },
  {
    "path": "backend/.flake8",
    "content": "[flake8]\nmax-line-length = 88\nmax-complexity = 100\nselect = B,C,E,F,W,T\nignore = E203, W503, E501"
  },
  {
    "path": "backend/.gcloudignore",
    "content": "# This file specifies files that are *not* uploaded to Google Cloud Platform\n# using gcloud. It follows the same syntax as .gitignore, with the addition of\n# \"#!include\" directives (which insert the entries of the given .gitignore-style\n# file at that point).\n#\n# For more information, run:\n#   $ gcloud topic gcloudignore\n#\n.gcloudignore\n# If you would like to upload your .git directory, .gitignore file or files\n# from your .gitignore file, remove the corresponding line\n# below:\n.git\n.gitignore\n\n# Python pycache:\n__pycache__/\n.venv\n.coverage\n.coveragerc\n.flake8\npoetry.lock\npyproject.toml\nREADME.md\n\n# Ignored by the build system\n/setup.cfg\n\n# remove irrelevant files/folders\ndeploy\ntests\n.env-template\n.pre-commit-config.yaml\ngcloud_key.json"
  },
  {
    "path": "backend/.pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/ambv/black\n    rev: 21.10b0\n    hooks:\n      - id: black\n        language_version: python3.11\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v2.3.0\n    hooks:\n      - id: flake8\n        args: [--config=./backend/.flake8]\n"
  },
  {
    "path": "backend/README.md",
    "content": "# Backend\n\n## Installation\n\n```\npoetry install\npoetry run pre-commit install\n```\n\n## Run Locally\n\nNavigate to localhost:8000\n\n```\nyarn start\n```\n\n## Test with Code Coverage\n\n```\nyarn test\n```\n\nView coverage with GitHub badge or on coveralls.io\n\n## Build\n\nIf a new requirement has been added, make sure to update the requirements.txt\n\n```\nyarn set-reqs\n```\n\nThen, just commit on the main branch (Google Cloud Run takes care of the rest)\n\n## Adding a Secret\n\nUpdate cloudbuild.yaml, .env, .env-template, and GCP Cloud Run Trigger Substitution Variables.\n"
  },
  {
    "path": "backend/deploy/app.yaml",
    "content": "service: default\nruntime: python311\nentrypoint: gunicorn -w 2 -k uvicorn.workers.UvicornWorker src.main:app\n\n#smallest instance class\ninstance_class: F1\n\n#prevents creating additional instances\nautomatic_scaling:\n  min_instances: 0\n  max_instances: 1\n\nenv_variables:\n  PROD: true\n"
  },
  {
    "path": "backend/deploy/cloudbuild.yaml",
    "content": "steps:\n  - name: node:10.15.1\n    entrypoint: npm\n    args: [\"install\"]\n    dir: \"backend\"\n  - name: node:10.15.1\n    entrypoint: npm\n    args: [\"run\", \"create-env\"]\n    dir: \"backend\"\n    env:\n      - \"DEV_OAUTH_CLIENT_ID=${_DEV_OAUTH_CLIENT_ID}\"\n      - \"DEV_OAUTH_CLIENT_SECRET=${_DEV_OAUTH_CLIENT_SECRET}\"\n      - \"DEV_OAUTH_REDIRECT_URI=${_DEV_OAUTH_REDIRECT_URI}\"\n      - \"PROD_OAUTH_CLIENT_ID=${_PROD_OAUTH_CLIENT_ID}\"\n      - \"PROD_OAUTH_CLIENT_SECRET=${_PROD_OAUTH_CLIENT_SECRET}\"\n      - \"PROD_OAUTH_REDIRECT_URI=${_PROD_OAUTH_REDIRECT_URI}\"\n      - \"MONGODB_PASSWORD=${_MONGODB_PASSWORD}\"\n      - \"SENTRY_DSN=${_SENTRY_DSN}\"\n  - name: \"gcr.io/cloud-builders/gcloud\"\n    args: [\"app\", \"deploy\", \"--appyaml\", \"./deploy/app.yaml\"]\n    dir: \"backend\"\n"
  },
  {
    "path": "backend/deploy/dispatch.yaml",
    "content": "dispatch:\n  - url: \"*/.*\"\n    service: default\n"
  },
  {
    "path": "backend/package.json",
    "content": "{\n  \"name\": \"github-trends\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"scripts\": {\n    \"gen-lang-map\": \"poetry run python src/data/github/language_map.py\",\n    \"start\": \"poetry run uvicorn src.main:app --reload --port=8000\",\n    \"set-reqs\": \"poetry lock && poetry export -f requirements.txt --output requirements.txt --without-hashes\",\n    \"create-env\": \"printenv > .env\",\n    \"test\": \"poetry run coverage run --source=src -m unittest -v && poetry run coverage report\",\n    \"isort\": \"poetry run isort . --src-path=./src --multi-line=3 --trailing-comma --line-length=88 --combine-as --ensure-newline-before-comments\"\n  }\n}\n"
  },
  {
    "path": "backend/pyproject.toml",
    "content": "[tool.poetry]\nname = \"github-trends\"\nversion = \"0.1.0\"\ndescription = \"\"\nauthors = [\"Abhijit Gupta <avgupta456@gmail.com>\"]\nlicense = \"MIT\"\n\n[tool.poetry.dependencies]\npython = \"^3.11\"\nfastapi = \"^0.104.1\"\nuvicorn = {extras = [\"standard\"], version = \"^0.24.0.post1\"}\nrequests = \"^2.31.0\"\npython-dotenv = \"^1.0.0\"\nmotor = \"^3.3.1\"\naiofiles = \"^23.2.1\"\naiounittest = \"^1.4.2\"\ncoveralls = \"^3.3.1\"\ngrpcio = \"^1.59.2\"\ngunicorn = \"^21.2.0\"\npymongo = {extras = [\"srv\"], version = \"^4.6.0\"}\npytz = \"^2023.3.post1\"\nsentry-sdk = \"^1.34.0\"\nsvgwrite = \"^1.4.3\"\n\n[tool.poetry.dev-dependencies]\n\n[tool.poetry.group.dev.dependencies]\nblack = \"^23.11.0\"\nflake8 = \"^6.1.0\"\nisort = \"^5.12.0\"\npre-commit = \"^3.5.0\"\npyinstrument = \"^4.6.1\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "backend/requirements.txt",
    "content": "aiofiles==23.2.1\naiounittest==1.4.2\nannotated-types==0.6.0\nanyio==3.7.1\ncertifi==2023.11.17\ncharset-normalizer==3.3.2\nclick==8.1.7\ncolorama==0.4.6\ncoverage==6.5.0\ncoveralls==3.3.1\ndnspython==2.4.2\ndocopt==0.6.2\nfastapi==0.104.1\ngrpcio==1.59.3\ngunicorn==21.2.0\nh11==0.14.0\nhttptools==0.6.1\nidna==3.4\nmotor==3.3.2\npackaging==23.2\npydantic-core==2.14.5\npydantic==2.5.2\npymongo==4.6.0\npymongo[srv]==4.6.0\npython-dotenv==1.0.0\npytz==2023.3.post1\npyyaml==6.0.1\nrequests==2.31.0\nsentry-sdk==1.36.0\nsniffio==1.3.0\nstarlette==0.27.0\nsvgwrite==1.4.3\ntyping-extensions==4.8.0\nurllib3==2.1.0\nuvicorn[standard]==0.24.0.post1\nuvloop==0.19.0\nwatchfiles==0.21.0\nwebsockets==12.0\nwrapt==1.16.0\n"
  },
  {
    "path": "backend/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "backend/scripts/delete_old_data.py",
    "content": "import asyncio\nimport os\nimport sys\nfrom datetime import datetime\nfrom typing import Any\n\nfrom dotenv import find_dotenv, load_dotenv\n\nload_dotenv(find_dotenv())\n\n# Add the parent directory to the Python path\nsys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\")))\n\n# flake8: noqa E402\n\nfrom src.constants import API_VERSION\nfrom src.data.mongo.main import USER_MONTHS\n\n\ndef get_filters(cutoff_date: datetime) -> Any:\n    return {\n        \"$or\": [\n            {\"month\": {\"$lte\": cutoff_date}},\n            {\"version\": {\"$ne\": API_VERSION}},\n        ],\n    }\n\n\nasync def count_old_rows(cutoff_date: datetime) -> int:\n    filters = get_filters(cutoff_date)\n    num_rows = len(await USER_MONTHS.find(filters).to_list(length=None))  # type: ignore\n    return num_rows\n\n\nasync def delete_old_rows(cutoff_date: datetime):\n    filters = get_filters(cutoff_date)\n    result = await USER_MONTHS.delete_many(filters)\n    print(f\"Deleted {result.deleted_count} rows\")\n\n\nasync def main():\n    # Replace 'your_date_field' with the actual name of your date field\n    cutoff_date = datetime(2024, 12, 31)\n\n    count = await count_old_rows(cutoff_date)\n    if count == 0:\n        print(\"No rows to delete.\")\n        return\n\n    print(f\"Found {count} rows to delete.\")\n    print()\n\n    confirmation = input(\"Are you sure you want to delete these rows? (yes/no): \")\n    if confirmation.lower() != \"yes\":\n        print(\"Operation canceled.\")\n        return\n\n    print()\n    await delete_old_rows(cutoff_date)\n\n\nif __name__ == \"__main__\":\n    loop = asyncio.get_event_loop()\n    loop.run_until_complete(main())\n"
  },
  {
    "path": "backend/scripts/local.py",
    "content": "import argparse\nimport asyncio\nimport json\nimport os\nimport sys\nfrom datetime import datetime\n\n# Add the parent directory to the Python path\nsys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\")))\nos.environ[\"LOCAL\"] = \"True\"\n\n# flake8: noqa E402\n\nfrom src.aggregation.layer0 import get_user_data\nfrom src.processing.user import get_top_languages, get_top_repos\nfrom src.processing.wrapped.package import get_wrapped_data\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(description=\"GitHub Trends Script\")\n\n    parser.add_argument(\"--user_id\", required=True, help=\"GitHub user ID\", type=str)\n    parser.add_argument(\n        \"--access_token\", required=True, help=\"GitHub access token\", type=str\n    )\n    parser.add_argument(\n        \"--start_date\",\n        default=\"2023-01-01\",\n        help=\"Start date in YYYY-MM-DD format\",\n        type=str,\n    )\n    parser.add_argument(\n        \"--end_date\",\n        default=\"2023-01-31\",\n        help=\"End date in YYYY-MM-DD format\",\n        type=str,\n    )\n    parser.add_argument(\n        \"--timezone\", default=\"America/New_York\", help=\"Timezone\", type=str\n    )\n    parser.add_argument(\n        \"--output_dir\", default=\"./\", help=\"Output directory path\", type=str\n    )\n\n    return parser.parse_args()\n\n\nasync def main():\n    args = parse_args()\n\n    start_date = datetime.strptime(args.start_date, \"%Y-%m-%d\")\n    end_date = datetime.strptime(args.end_date, \"%Y-%m-%d\")\n\n    print(\"Local script running...\")\n    print(\"User ID:\", args.user_id)\n    print(\"Access token:\", args.access_token)\n    print(\"Start date:\", start_date)\n    print(\"End date:\", end_date)\n    print(\"Timezone:\", args.timezone)\n    print(\"Output directory:\", args.output_dir)\n    print()\n\n    raw_output = await get_user_data(\n        args.user_id, start_date, end_date, args.timezone, args.access_token\n    )\n\n    with open(os.path.join(args.output_dir, \"raw.json\"), \"w\") as f:\n        f.write(raw_output.model_dump_json(indent=2))\n\n    langs_output = get_top_languages(\n        raw_output, loc_metric=\"changed\", include_private=True\n    )\n\n    langs_output = (\n        [json.loads(x.model_dump_json()) for x in langs_output[0]],\n        langs_output[1],\n    )\n\n    repos_output = get_top_repos(\n        raw_output, loc_metric=\"changed\", include_private=True, group=\"none\"\n    )\n\n    repos_output = (\n        [json.loads(x.model_dump_json()) for x in repos_output[0]],\n        repos_output[1],\n    )\n\n    with open(os.path.join(args.output_dir, \"langs.json\"), \"w\") as f:\n        f.write(json.dumps(langs_output, indent=2))\n\n    with open(os.path.join(args.output_dir, \"repos.json\"), \"w\") as f:\n        f.write(json.dumps(repos_output, indent=2))\n\n    wrapped_user = get_wrapped_data(raw_output, 2023)\n\n    with open(os.path.join(args.output_dir, \"wrapped.json\"), \"w\") as f:\n        f.write(wrapped_user.model_dump_json(indent=2))\n\n    print(\"Wrote output to\", args.output_dir)\n\n\nif __name__ == \"__main__\":\n    loop = asyncio.get_event_loop()\n    loop.run_until_complete(main())\n"
  },
  {
    "path": "backend/src/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/aggregation/layer0/__init__.py",
    "content": "from src.aggregation.layer0.package import get_user_data\n\n__all__ = [\"get_user_data\"]\n"
  },
  {
    "path": "backend/src/aggregation/layer0/contributions.py",
    "content": "from collections import defaultdict\nfrom datetime import date, datetime\nfrom typing import Any, Dict, List, Optional, Tuple, Union\n\nimport pytz\n\nfrom src.aggregation.layer0.languages import CommitLanguages, get_commit_languages\nfrom src.constants import (\n    GRAPHQL_NODE_CHUNK_SIZE,\n    GRAPHQL_NODE_THREADS,\n    NODE_QUERIES,\n    PR_FILES,\n    REST_NODE_THREADS,\n)\nfrom src.data.github.graphql import (\n    RawCalendar,\n    RawCommit as GraphQLRawCommit,\n    RawEventsCommit,\n    RawEventsEvent,\n    RawRepo,\n    get_commits,\n    get_repo,\n    get_user_contribution_calendar,\n    get_user_contribution_events,\n)\nfrom src.data.github.rest import (\n    RawCommit as RESTRawCommit,\n    RawCommitFile,\n    get_commit_files,\n    get_repo_commits,\n)\nfrom src.models import UserContributions\nfrom src.utils import date_to_datetime, gather\n\n\nclass ContribsList:\n    def __init__(self):\n        self.commits: List[RawEventsCommit] = []\n        self.issues: List[RawEventsEvent] = []\n        self.prs: List[RawEventsEvent] = []\n        self.reviews: List[RawEventsEvent] = []\n        self.repos: List[RawEventsEvent] = []\n\n    def add(self, label: str, event: Union[RawEventsCommit, RawEventsEvent]):\n        if label == \"commit\" and isinstance(event, RawEventsCommit):\n            self.commits.append(event)\n        elif label == \"issue\" and isinstance(event, RawEventsEvent):\n            self.issues.append(event)\n        elif label == \"pr\" and isinstance(event, RawEventsEvent):\n            self.prs.append(event)\n        elif label == \"review\" and isinstance(event, RawEventsEvent):\n            self.reviews.append(event)\n        elif label == \"repo\" and isinstance(event, RawEventsEvent):\n            self.repos.append(event)\n\n\ndef get_user_all_contribution_events(\n    user_id: str,\n    start_date: datetime,\n    end_date: datetime,\n    access_token: Optional[str] = None,\n) -> Dict[str, ContribsList]:\n    repo_contribs: Dict[str, ContribsList] = defaultdict(lambda: ContribsList())\n    after: Optional[str] = \"\"\n    cont = True\n    while cont:\n        after_str = after if isinstance(after, str) else \"\"\n        response = get_user_contribution_events(\n            user_id=user_id,\n            start_date=start_date,\n            end_date=end_date,\n            after=after_str,\n            access_token=access_token,\n        )\n\n        cont = False\n        node_lists = [\n            (\"commit\", response.commit_contribs_by_repo),\n            (\"issue\", response.issue_contribs_by_repo),\n            (\"pr\", response.pr_contribs_by_repo),\n            (\"review\", response.review_contribs_by_repo),\n        ]\n        for event_type, event_list in node_lists:\n            for repo in event_list:\n                name = repo.repo.name\n                for event in repo.contribs.nodes:\n                    repo_contribs[name].add(event_type, event)\n                if repo.contribs.page_info.has_next_page:\n                    after = repo.contribs.page_info.end_cursor\n                    cont = True\n\n        for repo in response.repo_contribs.nodes:\n            name = repo.repo.name\n            node = RawEventsEvent(occurredAt=repo.occurred_at)\n            repo_contribs[name].add(\"repo\", node)\n\n    return repo_contribs\n\n\ndef get_all_commit_info(\n    user_id: str,\n    name_with_owner: str,\n    start_date: datetime,\n    end_date: datetime,\n    access_token: Optional[str] = None,\n) -> List[RESTRawCommit]:\n    owner, repo = name_with_owner.split(\"/\")\n    data: List[RESTRawCommit] = []\n    for i in range(10):\n        if len(data) == 100 * i:\n            new_data = get_repo_commits(\n                owner, repo, user_id, start_date, end_date, i + 1, access_token\n            )\n            data.extend(new_data)\n\n    # sort ascending\n    return sorted(data, key=lambda x: x.timestamp)\n\n\nasync def get_all_commit_languages(\n    commit_infos: List[List[RESTRawCommit]],\n    repos: List[str],\n    repo_infos: Dict[str, RawRepo],\n    access_token: Optional[str] = None,\n    catch_errors: bool = False,\n) -> Tuple[Dict[str, List[datetime]], Dict[str, List[CommitLanguages]]]:\n    commit_node_ids = [[x.node_id for x in repo] for repo in commit_infos]\n    commit_times = [[x.timestamp for x in repo] for repo in commit_infos]\n\n    id_mapping: Dict[str, Tuple[int, int]] = {}\n    repo_mapping: Dict[str, str] = {}\n    all_node_ids: List[str] = []\n    for i, repo_node_ids in enumerate(commit_node_ids):\n        for j, node_id in enumerate(repo_node_ids):\n            id_mapping[node_id] = (i, j)\n            repo_mapping[node_id] = repos[i]\n            all_node_ids.append(node_id)\n\n    node_id_chunks: List[List[str]] = [\n        all_node_ids[i : min(len(all_node_ids), i + GRAPHQL_NODE_CHUNK_SIZE)]\n        for i in range(0, len(all_node_ids), GRAPHQL_NODE_CHUNK_SIZE)\n    ]\n\n    commit_language_chunks: List[List[Optional[GraphQLRawCommit]]] = await gather(\n        funcs=[get_commits for _ in node_id_chunks],\n        args_dicts=[\n            {\n                \"node_ids\": node_id_chunk,\n                \"access_token\": access_token,\n                \"catch_errors\": catch_errors,\n            }\n            for node_id_chunk in node_id_chunks\n        ],\n        max_threads=GRAPHQL_NODE_THREADS,\n    )\n\n    temp_commit_languages: List[Optional[GraphQLRawCommit]] = []\n    for commit_language_chunk in commit_language_chunks:\n        temp_commit_languages.extend(commit_language_chunk)\n\n    # returns commits with no associated PR or incomplete PR\n    filtered_commits: List[GraphQLRawCommit] = filter(\n        lambda x: x is not None\n        and (len(x.prs.nodes) == 0 or x.prs.nodes[0].changed_files > PR_FILES)\n        and (x.additions + x.deletions > 100),\n        temp_commit_languages,\n    )  # type: ignore\n\n    # get NODE_QUERIES largest commits with no associated PR or incomplete PR\n    sorted_commits = sorted(\n        filtered_commits, key=lambda x: x.additions + x.deletions, reverse=True\n    )[:NODE_QUERIES]\n\n    sorted_commit_urls = [commit.url.split(\"/\") for commit in sorted_commits]\n    commit_files: List[List[RawCommitFile]] = await gather(\n        funcs=[get_commit_files for _ in sorted_commit_urls],\n        args_dicts=[\n            {\n                \"owner\": url[3],\n                \"repo\": url[4],\n                \"sha\": url[6],\n                \"access_token\": access_token,\n            }\n            for url in sorted_commit_urls\n        ],\n        max_threads=REST_NODE_THREADS,\n    )\n\n    commit_files_dict: Dict[str, List[RawCommitFile]] = {\n        commit.url: commit_file\n        for commit, commit_file in zip(sorted_commits, commit_files)\n    }\n\n    commit_languages: List[List[CommitLanguages]] = [\n        [CommitLanguages() for _ in repo] for repo in commit_infos\n    ]\n    for raw_commits, node_ids in zip(commit_language_chunks, node_id_chunks):\n        for raw_commit, node_id in zip(raw_commits, node_ids):\n            curr_commit_files: Optional[List[RawCommitFile]] = None\n            if raw_commit is not None and raw_commit.url in commit_files_dict:\n                curr_commit_files = commit_files_dict[raw_commit.url]\n            lang_breakdown = get_commit_languages(\n                raw_commit, curr_commit_files, repo_infos[repo_mapping[node_id]]\n            )\n            i, j = id_mapping[node_id]\n            commit_languages[i][j] = lang_breakdown\n\n    commit_times_dict: Dict[str, List[datetime]] = {}\n    commit_languages_dict: Dict[str, List[CommitLanguages]] = {}\n    for repo, times, languages in zip(repos, commit_times, commit_languages):\n        commit_times_dict[repo] = times\n        commit_languages_dict[repo] = languages\n\n    return commit_times_dict, commit_languages_dict\n\n\nasync def get_cleaned_contributions(\n    user_id: str,\n    start_date: datetime,\n    end_date: datetime,\n    access_token: Optional[str],\n    catch_errors: bool = False,\n) -> Tuple[\n    RawCalendar,\n    Dict[str, ContribsList],\n    Dict[str, RawRepo],\n    Dict[str, List[datetime]],\n    Dict[str, List[CommitLanguages]],\n]:\n    calendar = get_user_contribution_calendar(\n        user_id, start_date, end_date, access_token\n    )\n    contrib_events = get_user_all_contribution_events(\n        user_id, start_date, end_date, access_token\n    )\n    repos: List[str] = list(set(contrib_events.keys()))\n\n    commit_infos: List[List[RESTRawCommit]] = await gather(\n        funcs=[get_all_commit_info for _ in repos],\n        args_dicts=[\n            {\n                \"user_id\": user_id,\n                \"name_with_owner\": repo,\n                \"start_date\": start_date,\n                \"end_date\": end_date,\n                \"access_token\": access_token,\n            }\n            for repo in repos\n        ],\n        max_threads=REST_NODE_THREADS,\n    )\n\n    _repo_infos: List[Optional[RawRepo]] = await gather(\n        funcs=[get_repo for _ in repos],\n        args_dicts=[\n            {\n                \"owner\": repo.split(\"/\")[0],\n                \"repo\": repo.split(\"/\")[1],\n                \"access_token\": access_token,\n                \"catch_errors\": catch_errors,\n            }\n            for repo in repos\n        ],\n        max_threads=REST_NODE_THREADS,\n    )\n\n    repo_infos = {k: v for k, v in zip(repos, _repo_infos) if v is not None}\n\n    commit_times_dict, commit_languages_dict = await get_all_commit_languages(\n        commit_infos,\n        repos,\n        repo_infos,\n        access_token,\n        catch_errors,\n    )\n\n    return (\n        calendar,\n        contrib_events,\n        repo_infos,\n        commit_times_dict,\n        commit_languages_dict,\n    )\n\n\nclass StatsContainer:\n    def __init__(self):\n        self.contribs: int = 0\n        self.commits: int = 0\n        self.issues: int = 0\n        self.prs: int = 0\n        self.reviews: int = 0\n        self.repos: int = 0\n        self.other: int = 0\n        self.languages = CommitLanguages()\n\n    def add_stat(self, label: str, count: int, add: bool = False) -> None:\n        if label == \"commit\":\n            self.commits += count\n        elif label == \"issue\":\n            self.issues += count\n        elif label == \"pr\":\n            self.prs += count\n        elif label == \"review\":\n            self.reviews += count\n        elif label == \"repo\":\n            self.repos += count\n\n        if add:\n            self.contribs += count\n        else:\n            self.other -= count\n\n    def to_dict(self) -> Dict[str, Any]:\n        return {\n            \"contribs_count\": self.contribs,\n            \"commits_count\": self.commits,\n            \"issues_count\": self.issues,\n            \"prs_count\": self.prs,\n            \"reviews_count\": self.reviews,\n            \"repos_count\": self.repos,\n            \"other_count\": self.other,\n            \"languages\": self.languages.to_dict(),\n        }\n\n\nclass ListsContainer:\n    def __init__(self):\n        self.commits: List[datetime] = []\n        self.issues: List[datetime] = []\n        self.prs: List[datetime] = []\n        self.reviews: List[datetime] = []\n        self.repos: List[datetime] = []\n\n    def add_list(self, label: str, times: List[datetime]) -> None:\n        if label == \"commit\":\n            self.commits.extend(times)\n        elif label == \"issue\":\n            self.issues.extend(times)\n        elif label == \"pr\":\n            self.prs.extend(times)\n        elif label == \"review\":\n            self.reviews.extend(times)\n        elif label == \"repo\":\n            self.repos.extend(times)\n\n    def to_dict(self) -> Dict[str, Any]:\n        return {\n            \"commits\": self.commits,\n            \"issues\": self.issues,\n            \"prs\": self.prs,\n            \"reviews\": self.reviews,\n            \"repos\": self.repos,\n        }\n\n\nclass DateContainer:\n    def __init__(self):\n        self.date = \"\"\n        self.weekday = 0\n        self.stats = StatsContainer()\n        self.lists = ListsContainer()\n\n    def add_stat(\n        self, label: str, count: int, times: List[datetime], add: bool = False\n    ):\n        self.stats.add_stat(label, count, add)\n        self.lists.add_list(label, times)\n\n    def to_dict(self) -> Dict[str, Any]:\n        return {\n            \"date\": self.date,\n            \"weekday\": self.weekday,\n            \"stats\": self.stats.to_dict(),\n            \"lists\": self.lists.to_dict(),\n        }\n\n\n# assumed one month span, can be no more than one year\nasync def get_contributions(\n    user_id: str,\n    start_date: date,\n    end_date: date,\n    timezone_str: str = \"US/Eastern\",\n    access_token: Optional[str] = None,\n    catch_errors: bool = False,\n) -> UserContributions:\n    tz = pytz.timezone(timezone_str)\n\n    start_month = date_to_datetime(start_date)\n    end_month = date_to_datetime(end_date, hour=23, minute=59, second=59)\n    (\n        calendar,\n        contrib_events,\n        repo_infos,\n        commit_times_dict,\n        commit_languages_dict,\n    ) = await get_cleaned_contributions(\n        user_id, start_month, end_month, access_token, catch_errors\n    )\n\n    total_stats = StatsContainer()\n    public_stats = StatsContainer()\n    total: Dict[str, DateContainer] = defaultdict(DateContainer)\n    public: Dict[str, DateContainer] = defaultdict(DateContainer)\n    repo_stats: Dict[str, StatsContainer] = defaultdict(StatsContainer)\n    repositories: Dict[str, Dict[str, DateContainer]] = defaultdict(\n        lambda: defaultdict(DateContainer)\n    )\n\n    for week in calendar.weeks:\n        for day in week.contribution_days:\n            day_str = str(day.date)\n            for obj, stats_obj in [(total, total_stats), (public, public_stats)]:\n                obj[day_str].date = day.date.isoformat()\n                obj[day_str].weekday = day.weekday\n                obj[day_str].stats.contribs = day.count\n                obj[day_str].stats.other = day.count\n                stats_obj.contribs += day.count\n                stats_obj.other += day.count\n\n    def update_stats(\n        date_str: str, repo: str, event: str, count: int, times: List[datetime]\n    ):\n        # update global counts for this event\n        total[date_str].add_stat(event, count, times)\n        total_stats.add_stat(event, count)\n        if not repo_infos[repo].is_private:\n            public[date_str].add_stat(event, count, times)\n            public_stats.add_stat(event, count)\n        repositories[repo][date_str].add_stat(event, count, times, add=True)\n        repo_stats[repo].add_stat(event, count, add=True)\n\n    def update_langs(date_str: str, repo: str, langs: CommitLanguages):\n        stores = [\n            total[date_str].stats.languages,\n            total_stats.languages,\n            repositories[repo][date_str].stats.languages,\n            repo_stats[repo].languages,\n        ]\n        if not repo_infos[repo].is_private:\n            stores.append(public[date_str].stats.languages)\n            stores.append(public_stats.languages)\n\n        for store in stores:\n            store += langs\n\n    for repo, repo_events in contrib_events.items():\n        for label, events in [\n            (\"commit\", repo_events.commits),\n            (\"issue\", repo_events.issues),\n            (\"pr\", repo_events.prs),\n            (\"review\", repo_events.reviews),\n            (\"repo\", repo_events.repos),\n        ]:\n            events = sorted(events, key=lambda x: x.occurred_at)\n            for event in events:\n                datetime_obj = event.occurred_at.astimezone(tz)\n                date_str = datetime_obj.date().isoformat()\n                repositories[repo][date_str].date = date_str\n                if isinstance(event, RawEventsCommit):\n                    count = 0\n                    commit_times: List[datetime] = []\n                    while len(commit_languages_dict[repo]) > 0 and count < event.count:\n                        commit_times.append(commit_times_dict[repo].pop(0))\n                        langs = commit_languages_dict[repo].pop(0)\n                        update_langs(date_str, repo, langs)\n                        count += 1\n                    update_stats(date_str, repo, \"commit\", event.count, commit_times)\n                else:\n                    update_stats(date_str, repo, label, 1, [datetime_obj])\n\n    total_stats_dict = total_stats.to_dict()\n    public_stats_dict = public_stats.to_dict()\n    repo_stats_dict = {name: stats.to_dict() for name, stats in repo_stats.items()}\n\n    for repo in repo_stats:\n        repo_stats_dict[repo][\"private\"] = repo_infos[repo].is_private\n\n    total_list = [v.to_dict() for v in total.values() if v.stats.contribs > 0]\n    public_list = [v.to_dict() for v in public.values() if v.stats.contribs > 0]\n    repositories_list = {\n        name: [v.to_dict() for v in repo.values()]\n        for name, repo in repositories.items()\n    }\n\n    output = UserContributions.model_validate(\n        {\n            \"total_stats\": total_stats_dict,\n            \"public_stats\": public_stats_dict,\n            \"total\": total_list,\n            \"public\": public_list,\n            \"repo_stats\": repo_stats_dict,\n            \"repos\": repositories_list,\n        }\n    )\n\n    return output\n"
  },
  {
    "path": "backend/src/aggregation/layer0/follows.py",
    "content": "from typing import List, Optional\n\nfrom src.data.github.graphql import (\n    get_user_followers as _get_user_followers,\n    get_user_following as _get_user_following,\n)\nfrom src.models import User, UserFollows\n\n\ndef get_user_follows(user_id: str, access_token: Optional[str]) -> UserFollows:\n    \"\"\"get user followers and users following for given user\"\"\"\n\n    followers: List[User] = []\n    following: List[User] = []\n\n    for user_list, get_func in zip(\n        [followers, following], [_get_user_followers, _get_user_following]\n    ):\n        after: Optional[str] = \"\"\n        index, cont = 0, True  # initialize variables\n        while cont and index < 10:\n            after_str: str = after if isinstance(after, str) else \"\"\n            data = get_func(user_id, after=after_str, access_token=access_token)\n\n            cont = False\n\n            user_list.extend(\n                map(\n                    lambda x: User(name=x.name, login=x.login, url=x.url),\n                    data.nodes,\n                )\n            )\n            if data.page_info.has_next_page:\n                after = data.page_info.end_cursor\n                cont = True\n\n            index += 1\n\n    return UserFollows(followers=followers, following=following)\n"
  },
  {
    "path": "backend/src/aggregation/layer0/languages.py",
    "content": "from json import load\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom src.constants import BLACKLIST, CUTOFF, DEFAULT_COLOR, FILE_CUTOFF\nfrom src.data.github.graphql import RawCommit, RawRepo\nfrom src.data.github.rest import RawCommitFile\n\nEXTENSIONS: Dict[str, Dict[str, str]] = load(open(\"./src/data/github/extensions.json\"))\n\n\nclass CommitLanguages:\n    def __init__(self):\n        self.langs: Dict[str, Dict[str, Union[str, int]]] = {}\n\n    def __repr__(self):\n        return str(self.langs)\n\n    def add_lines(\n        self, name: str, color: Optional[str], additions: int, deletions: int\n    ):\n        if (\n            name not in BLACKLIST\n            and max(additions, deletions) > 0\n            and max(additions, deletions) < FILE_CUTOFF\n        ):\n            color = color or DEFAULT_COLOR\n            if name not in self.langs:\n                self.langs[name] = {\"color\": color, \"additions\": 0, \"deletions\": 0}\n            self.langs[name][\"additions\"] += additions  # type: ignore\n            self.langs[name][\"deletions\"] += deletions  # type: ignore\n\n    def normalize(self, add_ratio: float, del_ratio: float):\n        for lang in self.langs:\n            new_add = round(int(self.langs[lang][\"additions\"]) * add_ratio)\n            self.langs[lang][\"additions\"] = new_add\n            new_del = round(int(self.langs[lang][\"deletions\"]) * del_ratio)\n            self.langs[lang][\"deletions\"] = new_del\n\n    def __add__(self, other: \"CommitLanguages\"):\n        for lang in other.langs:\n            if lang not in self.langs:\n                self.langs[lang] = other.langs[lang].copy()\n            else:\n                self.langs[lang][\"additions\"] += other.langs[lang][\"additions\"]  # type: ignore\n                self.langs[lang][\"deletions\"] += other.langs[lang][\"deletions\"]  # type: ignore\n\n    def to_dict(self) -> Dict[str, Any]:\n        return self.langs\n\n\ndef get_commit_languages(\n    commit: Optional[RawCommit],\n    files: Optional[List[RawCommitFile]],\n    repo: RawRepo,\n) -> CommitLanguages:\n    out = CommitLanguages()\n\n    if commit is None:\n        return out\n\n    if max(commit.additions, commit.deletions) == 0:\n        return out\n\n    # assummed to be auto-generated or copied\n    if max(commit.additions, commit.deletions) > 10 * CUTOFF or (\n        max(commit.additions, commit.deletions) > CUTOFF\n        and min(commit.additions, commit.deletions) == 0\n    ):\n        return out\n\n    pr_coverage = 0\n    if len(commit.prs.nodes) > 0:\n        pr_obj = commit.prs.nodes[0]\n        pr_files = pr_obj.files.nodes\n        total_changed = sum(x.additions + x.deletions for x in pr_files)\n        pr_coverage = total_changed / max(1, (pr_obj.additions + pr_obj.deletions))\n\n    if files is not None:\n        for file in files:\n            filename = file.filename.split(\".\")\n            extension = \"\" if len(filename) <= 1 else filename[-1]\n            lang = EXTENSIONS.get(f\".{extension}\", None)\n            if lang is not None:\n                out.add_lines(\n                    lang[\"name\"], lang[\"color\"], file.additions, file.deletions\n                )\n    elif len(commit.prs.nodes) > 0 and pr_coverage > 0.25:\n        pr = commit.prs.nodes[0]\n        total_additions, total_deletions = 0, 0\n        for file in pr.files.nodes:\n            filename = file.path.split(\".\")\n            extension = \"\" if len(filename) <= 1 else filename[-1]\n            lang = EXTENSIONS.get(f\".{extension}\", None)\n            if lang is not None:\n                out.add_lines(\n                    lang[\"name\"], lang[\"color\"], file.additions, file.deletions\n                )\n            total_additions += file.additions\n            total_deletions += file.deletions\n        add_ratio = min(pr.additions, commit.additions) / max(1, total_additions)\n        del_ratio = min(pr.deletions, commit.deletions) / max(1, total_deletions)\n        out.normalize(add_ratio, del_ratio)\n    elif commit.additions + commit.deletions > 2 * CUTOFF:\n        # assummed to be auto generated\n        return out\n    else:\n        repo_info = repo.languages.edges\n        languages = [x for x in repo_info if x.node.name not in BLACKLIST]\n        total_repo_size = sum(language.size for language in languages)\n        for language in languages:\n            lang_name = language.node.name\n            lang_color = language.node.color\n            lang_size = language.size\n            additions = round(commit.additions * lang_size / total_repo_size)\n            deletions = round(commit.deletions * lang_size / total_repo_size)\n            out.add_lines(lang_name, lang_color, additions, deletions)\n\n    return out\n"
  },
  {
    "path": "backend/src/aggregation/layer0/package.py",
    "content": "from datetime import date\nfrom typing import Optional\n\nfrom src.aggregation.layer0.contributions import get_contributions\nfrom src.models import UserPackage\n\n# from src.subscriber.aggregation.user.follows import get_user_follows\n\n\nasync def get_user_data(\n    user_id: str,\n    start_date: date,\n    end_date: date,\n    timezone_str: str,\n    access_token: Optional[str],\n    catch_errors: bool = False,\n) -> UserPackage:\n    \"\"\"packages all processing steps for the user query\"\"\"\n\n    contribs = await get_contributions(\n        user_id=user_id,\n        start_date=start_date,\n        end_date=end_date,\n        timezone_str=timezone_str,\n        access_token=access_token,\n        catch_errors=catch_errors,\n    )\n    return UserPackage(contribs=contribs)\n"
  },
  {
    "path": "backend/src/aggregation/layer1/__init__.py",
    "content": "from src.aggregation.layer1.user import query_user\n\n__all__ = [\"query_user\"]\n"
  },
  {
    "path": "backend/src/aggregation/layer1/auth.py",
    "content": "from datetime import timedelta\nfrom typing import List, Optional, Tuple\n\nfrom src.constants import OWNER, REPO\nfrom src.data.github.rest import (\n    RESTError,\n    RESTErrorNotFound,\n    get_repo_stargazers as github_get_repo_stargazers,\n    get_user as github_get_user,\n    get_user_starred_repos as github_get_user_starred_repos,\n)\nfrom src.data.github.utils import get_access_token\nfrom src.data.mongo.user import get_public_user as db_get_public_user\nfrom src.utils import alru_cache\n\n\nasync def get_valid_github_user(user_id: str) -> Optional[str]:\n    access_token = get_access_token()\n    try:\n        return github_get_user(user_id, access_token)[\"login\"]\n    except (RESTErrorNotFound, KeyError):\n        # User does not exist\n        return None\n    except RESTError:\n        # Rate limited, so assume user exists\n        return user_id\n\n\nasync def get_valid_db_user(user_id: str) -> bool:\n    user = await db_get_public_user(user_id)\n    return user is not None\n\n\n@alru_cache(ttl=timedelta(minutes=15))\nasync def get_repo_stargazers(\n    owner: str = OWNER, repo: str = REPO, no_cache: bool = False\n) -> Tuple[bool, List[str]]:\n    access_token = get_access_token()\n    data: List[str] = []\n    page = 0\n    while len(data) == 100 * page:\n        temp_data = github_get_repo_stargazers(access_token, owner, repo, page=page)\n        temp_data = [x[\"user\"][\"login\"] for x in temp_data]\n        data.extend(temp_data)\n        page += 1\n\n    return (True, data)\n\n\nasync def get_user_stars(user_id: str) -> List[str]:\n    access_token = get_access_token()\n    try:\n        data = github_get_user_starred_repos(user_id, access_token)\n        data = [x[\"repo\"][\"full_name\"] for x in data]\n        return data\n    except RESTErrorNotFound:\n        # User does not exist (and rate limited previously)\n        return []\n    except RESTError:\n        # Rate limited, so assume user starred repo\n        return [f\"{OWNER}/{REPO}\"]\n"
  },
  {
    "path": "backend/src/aggregation/layer1/user.py",
    "content": "from calendar import monthrange\nfrom datetime import date, datetime, timedelta\nfrom typing import List, Optional, Tuple\n\nimport requests\n\nfrom src.aggregation.layer0.package import get_user_data\nfrom src.constants import API_VERSION  # , BACKEND_URL, PROD\nfrom src.data.github.graphql import GraphQLErrorRateLimit\nfrom src.data.mongo.secret import update_keys\nfrom src.data.mongo.user_months import UserMonth, get_user_months, set_user_month\nfrom src.models.user.main import UserPackage\nfrom src.utils import alru_cache, date_to_datetime\n\ns = requests.Session()\n\n\n# Formerly the subscriber, compute and save new data here\n\n\nasync def query_user_month(\n    user_id: str,\n    access_token: Optional[str],\n    private_access: bool,\n    start_date: date,\n    retries: int = 0,\n) -> Optional[UserMonth]:\n    year, month = start_date.year, start_date.month\n    end_day = monthrange(year, month)[1]\n    end_date = date(year, month, end_day)\n\n    try:\n        data = await get_user_data(\n            user_id=user_id,\n            start_date=start_date,\n            end_date=end_date,\n            timezone_str=\"US/Eastern\",\n            access_token=access_token,\n            catch_errors=retries > 0,\n        )\n    except GraphQLErrorRateLimit:\n        return None\n    except Exception:\n        # Retry, catching exceptions and marking incomplete this time\n        if retries < 1:\n            await query_user_month(\n                user_id, access_token, private_access, start_date, retries + 1\n            )\n        return None\n\n    month_completed = datetime.now() > date_to_datetime(end_date) + timedelta(days=1)\n    user_month = UserMonth.model_validate(\n        {\n            \"user_id\": user_id,\n            \"month\": date_to_datetime(start_date),\n            \"version\": API_VERSION,\n            \"private\": private_access,\n            \"complete\": retries == 0 and month_completed,\n            \"data\": data,\n        }\n    )\n\n    await set_user_month(user_month)\n    return user_month\n\n\n@alru_cache(ttl=timedelta(hours=6))\nasync def query_user(\n    user_id: str,\n    access_token: Optional[str],\n    private_access: bool = False,\n    start_date: date = date.today() - timedelta(365),\n    end_date: date = date.today(),\n    max_time: int = 3600,  # seconds\n    no_cache: bool = False,\n) -> Tuple[bool, UserPackage]:\n    # Return (possibly incomplete) within max_time seconds\n    start_time = datetime.now()\n    incomplete = False\n\n    await update_keys()\n\n    curr_data: List[UserMonth] = await get_user_months(\n        user_id, private_access, start_date, end_date\n    )\n    curr_months = [x.month for x in curr_data if x.complete]\n\n    month, year = start_date.month, start_date.year\n    new_months: List[date] = []\n    while date(year, month, 1) <= end_date:\n        start = date(year, month, 1)\n        if date_to_datetime(start) not in curr_months:\n            new_months.append(start)\n        month = month % 12 + 1\n        year = year + (month == 1)\n\n    # Start with complete months and add any incomplete months\n    all_user_packages: List[UserPackage] = [x.data for x in curr_data if x.complete]\n    for month in new_months:\n        if datetime.now() - start_time < timedelta(seconds=max_time):\n            temp = await query_user_month(user_id, access_token, private_access, month)\n            if temp is not None:\n                all_user_packages.append(temp.data)\n            else:\n                incomplete = True\n        else:\n            incomplete = True\n\n    out: UserPackage = UserPackage.empty()\n    if len(all_user_packages) > 0:\n        out = all_user_packages[0]\n        for user_package in all_user_packages[1:]:\n            out += user_package\n        out.incomplete = incomplete\n\n    if incomplete or len(new_months) > 1:\n        # TODO: figure out why this causes an infinite loop\n        # # cache buster for publisher\n        # if PROD:\n        #     s.get(f\"{BACKEND_URL}/user/{user_id}?no_cache=True\")\n\n        return (False, out)\n\n    # only cache if just the current month updated\n    return (True, out)\n"
  },
  {
    "path": "backend/src/aggregation/layer2/__init__.py",
    "content": "from src.aggregation.layer2.auth import get_is_valid_user\nfrom src.aggregation.layer2.user import get_user, get_user_demo\n\n__all__ = [\"get_is_valid_user\", \"get_user\", \"get_user_demo\"]\n"
  },
  {
    "path": "backend/src/aggregation/layer2/auth.py",
    "content": "from datetime import timedelta\nfrom typing import Optional, Tuple\n\nfrom src.aggregation.layer1.auth import (\n    get_repo_stargazers,\n    get_user_stars,\n    get_valid_db_user,\n    get_valid_github_user,\n)\nfrom src.constants import OWNER, REPO, USER_BLACKLIST, USER_WHITELIST\nfrom src.data.github.rest import RESTError\nfrom src.utils import alru_cache\n\n\nasync def check_github_user_exists(user_id: str) -> Optional[str]:\n    return await get_valid_github_user(user_id)\n\n\nasync def check_db_user_exists(user_id: str) -> bool:\n    return await get_valid_db_user(user_id)\n\n\nasync def check_user_starred_repo(\n    user_id: str, owner: str = OWNER, repo: str = REPO\n) -> bool:\n    # Checks the repo's starred users (with cache)\n    try:\n        repo_stargazers = await get_repo_stargazers(owner, repo)\n        if user_id in repo_stargazers:\n            return True\n    except RESTError:\n        return True  # Assume the user has starred the repo\n\n    # Checks the user's 30 most recent starred repos (no cache)\n    user_stars = await get_user_stars(user_id)\n    return f\"{owner}/{repo}\" in user_stars\n\n\n@alru_cache(ttl=timedelta(hours=1))\nasync def get_is_valid_user(user_id: str) -> Tuple[bool, str]:\n    if user_id.lower() in USER_BLACKLIST:\n        # TODO: change error message\n        return (False, \"GitHub user not found\")\n\n    if user_id.lower() in USER_WHITELIST:\n        return (True, f\"Valid user {user_id.lower()}\")\n\n    valid_user_id = await check_github_user_exists(user_id)\n    if valid_user_id is None:\n        return (False, \"GitHub user not found\")\n\n    valid_db_user = await check_db_user_exists(valid_user_id)\n    user_starred = await check_user_starred_repo(valid_user_id)\n    if not (user_starred or valid_db_user):\n        return (False, \"Repo not starred\")\n\n    return (True, f\"Valid user {valid_user_id}\")\n"
  },
  {
    "path": "backend/src/aggregation/layer2/user.py",
    "content": "from datetime import date, timedelta\nfrom typing import Optional, Tuple\n\nfrom src.aggregation.layer0 import get_user_data\nfrom src.constants import USER_BLACKLIST\nfrom src.data.mongo.secret.functions import update_keys\nfrom src.data.mongo.user import PublicUserModel, get_public_user as db_get_public_user\nfrom src.data.mongo.user_months import get_user_months\nfrom src.models import UserPackage\nfrom src.models.background import UpdateUserBackgroundTask\nfrom src.utils import alru_cache\n\n# Formerly the publisher, loads existing data here\n\n\nasync def _get_user(\n    user_id: str, private_access: bool, start_date: date, end_date: date\n) -> Tuple[Optional[UserPackage], bool]:\n    user_months = await get_user_months(user_id, private_access, start_date, end_date)\n    if len(user_months) == 0:\n        return None, False\n\n    expected_num_months = (\n        (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1\n    )\n    complete = len(user_months) == expected_num_months\n\n    user_data = user_months[0].data\n    for user_month in user_months[1:]:\n        user_data += user_month.data\n\n    # TODO: handle timezone_str here\n    return user_data.trim(start_date, end_date), complete\n\n\n@alru_cache()\nasync def get_user(\n    user_id: str,\n    start_date: date,\n    end_date: date,\n    no_cache: bool = False,\n) -> Tuple[\n    bool, Tuple[Optional[UserPackage], bool, Optional[UpdateUserBackgroundTask]]\n]:\n    if user_id in USER_BLACKLIST:\n        return (False, (None, False, None))\n\n    user: Optional[PublicUserModel] = await db_get_public_user(user_id)\n    if user is None:\n        return (False, (None, False, None))\n\n    private_access = user.private_access or False\n    user_data, complete = await _get_user(user_id, private_access, start_date, end_date)\n    background_task = UpdateUserBackgroundTask(\n        user_id=user_id,\n        access_token=user.access_token,\n        private_access=private_access,\n        start_date=start_date,\n        end_date=end_date,\n    )\n    return (complete, (user_data, complete, background_task))\n\n\n@alru_cache(ttl=timedelta(minutes=15))\nasync def get_user_demo(\n    user_id: str, start_date: date, end_date: date, no_cache: bool = False\n) -> Tuple[bool, UserPackage]:\n    await update_keys()\n    timezone_str = \"US/Eastern\"\n    # recompute/cache but don't save to db\n    data = await get_user_data(\n        user_id=user_id,\n        start_date=start_date,\n        end_date=end_date,\n        timezone_str=timezone_str,\n        access_token=None,\n        catch_errors=True,\n    )\n    return (True, data)\n"
  },
  {
    "path": "backend/src/constants.py",
    "content": "import os\n\n# GLOBAL\nLOCAL = os.getenv(\"LOCAL\", \"False\") == \"True\"\nPROD = os.getenv(\"PROD\", \"False\") == \"True\"\nPROJECT_ID = \"github-334619\"\nBACKEND_URL = \"https://api.githubtrends.io\" if PROD else \"http://localhost:8000\"\n\nOWNER = \"avgupta456\"\nREPO = \"github-trends\"\n\n# API\n# https://docs.github.com/en/rest/reference/rate-limit\n# https://docs.github.com/en/rest/guides/best-practices-for-integrators#dealing-with-secondary-rate-limits\n# https://docs.github.com/en/graphql/overview/resource-limitations\nTIMEOUT = 15  # max seconds to wait for api response\nGRAPHQL_NODE_CHUNK_SIZE = 50  # number of nodes (commits) to query (max 100)\nGRAPHQL_NODE_THREADS = 5  # number of node queries simultaneously (avoid blacklisting)\nREST_NODE_THREADS = 50  # number of node queries simultaneously (avoid blacklisting)\n\nPR_FILES = 5  # max number of files to query for PRs\nNODE_QUERIES = 20  # max number of node queries to make\nCUTOFF = 1000  # if additions or deletions > CUTOFF, or sum > 2 * CUTOFF, ignore LOC\nFILE_CUTOFF = 1000  # if less than cutoff in file, count LOC\n\nAPI_VERSION = 0.02  # determines when to overwrite MongoDB data\n\n# CUSTOMIZATION\nBLACKLIST = [\"Jupyter Notebook\", \"HTML\"]  # languages to ignore\n\n# OAUTH\nprefix = \"PROD\" if PROD else \"DEV\"\n# client ID for GitHub OAuth App\nOAUTH_CLIENT_ID = os.getenv(f\"{prefix}_OAUTH_CLIENT_ID\", \"\")\n# client secret for App\nOAUTH_CLIENT_SECRET = os.getenv(f\"{prefix}_OAUTH_CLIENT_SECRET\", \"\")\n# redirect uri for App\nOAUTH_REDIRECT_URI = os.getenv(f\"{prefix}_OAUTH_REDIRECT_URI\", \"\")\n\n# MONGODB\nMONGODB_PASSWORD = os.getenv(\"MONGODB_PASSWORD\", \"\")\n\n# SVG\nDEFAULT_COLOR = \"#858585\"\n\n# SENTRY\nSENTRY_DSN = os.getenv(\"SENTRY_DSN\", \"\")\n\n# TESTING\nTEST_USER_ID = \"avgupta456\"\nTEST_REPO = \"github-trends\"\nTEST_TOKEN = os.getenv(\"AUTH_TOKEN\", \"\")  # for authentication\nTEST_NODE_IDS = [\n    \"C_kwDOENp939oAKGM1MzdlM2QzMTZjMmEyZGIyYWU4ZWI0MmNmNjQ4YWEwNWQ5OTBiMjM\",\n    \"C_kwDOD_-BVNoAKDFhNTIxNWE1MGM4ZDllOGEwYTFhNjhmYWZkYzE5MzA5YTRkMDMwZmM\",\n    \"C_kwDOD_-BVNoAKDRiZTQ4MTQ0MzgwYjBlNGEwNjQ4YjY4YWI4ZjFjYmQ3MWU4M2VhMzU\",\n]\nTEST_SHA = \"ad83e6340377904fa0295745b5314202b23d2f3f\"\n\n# WRAPPED\n\n# example users, don't need to star the repo\nUSER_WHITELIST = [\n    \"torvalds\",\n    \"yyx990803\",\n    \"shadcn\",\n    \"sindresorhus\",\n]\n\nUSER_BLACKLIST = [\"kangmingtay\", \"ae7er\", \"stalukdar7\", \"piyush7833\"]\n\nprint(\"PROD\", PROD)\nprint(\"API_VERSION\", API_VERSION)\nprint()\n"
  },
  {
    "path": "backend/src/data/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/data/github/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/data/github/auth/__init__.py",
    "content": "from src.data.github.auth.main import authenticate\n\n__all__ = [\"authenticate\"]\n"
  },
  {
    "path": "backend/src/data/github/auth/main.py",
    "content": "from datetime import datetime\nfrom typing import Dict, Optional, Tuple\n\nimport requests\n\nfrom src.constants import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI\n\ns = requests.session()\n\n\ndef get_unknown_user(access_token: str) -> Optional[str]:\n    \"\"\"\n    Accepts access_token and returns user_id of associated user\n    :param access_token: GitHub access token\n    :return: user_id or None if invalid access_token\n    \"\"\"\n    headers: Dict[str, str] = {\n        \"Accept\": \"application/vnd.github.v3+json\",\n        \"Authorization\": f\"bearer {access_token}\",\n    }\n\n    r = s.get(\"https://api.github.com/user\", params={}, headers=headers)\n    return r.json().get(\"login\", None)\n\n\nclass OAuthError(Exception):\n    pass\n\n\nasync def authenticate(code: str) -> Tuple[str, str]:\n    \"\"\"\n    Takes a authentication code, verifies, and returns user_id/access_token\n    :param code: GitHub authentication code from OAuth process\n    :return: user_id, access_token of authenticated user\n    \"\"\"\n    start = datetime.now()\n\n    params = {\n        \"client_id\": OAUTH_CLIENT_ID,\n        \"client_secret\": OAUTH_CLIENT_SECRET,\n        \"code\": code,\n        \"redirect_uri\": OAUTH_REDIRECT_URI,\n    }\n\n    r = s.post(\"https://github.com/login/oauth/access_token\", params=params)\n\n    if r.status_code != 200:\n        raise OAuthError(f\"OAuth Error: {str(r.status_code)}\")\n\n    access_token = r.text.split(\"&\")[0].split(\"=\")[1]\n    user_id = get_unknown_user(access_token)\n\n    if user_id is None:\n        raise OAuthError(\"OAuth Error: Invalid user_id/access_token\")\n\n    print(\"OAuth SignUp\", datetime.now() - start)\n    return user_id, access_token\n"
  },
  {
    "path": "backend/src/data/github/extensions.json",
    "content": "{\n    \".1\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".1in\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".1m\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".1x\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".2\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3in\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3m\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3p\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3pm\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3qt\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".3x\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".4\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".4dm\": {\n        \"color\": \"#004289\",\n        \"name\": \"4D\"\n    },\n    \".4th\": {\n        \"color\": \"#341708\",\n        \"name\": \"Forth\"\n    },\n    \".5\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".6\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".6pl\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".6pm\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".7\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".8\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".8xk\": {\n        \"color\": \"#A0AA87\",\n        \"name\": \"TI Program\"\n    },\n    \".8xk.txt\": {\n        \"color\": \"#A0AA87\",\n        \"name\": \"TI Program\"\n    },\n    \".8xp\": {\n        \"color\": \"#A0AA87\",\n        \"name\": \"TI Program\"\n    },\n    \".8xp.txt\": {\n        \"color\": \"#A0AA87\",\n        \"name\": \"TI Program\"\n    },\n    \".9\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".E\": {\n        \"color\": \"#ccce35\",\n        \"name\": \"E\"\n    },\n    \"._coffee\": {\n        \"color\": \"#244776\",\n        \"name\": \"CoffeeScript\"\n    },\n    \"._js\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \"._ls\": {\n        \"color\": \"#499886\",\n        \"name\": \"LiveScript\"\n    },\n    \".a51\": {\n        \"color\": \"#6E4C13\",\n        \"name\": \"Assembly\"\n    },\n    \".abap\": {\n        \"color\": \"#E8274B\",\n        \"name\": \"ABAP\"\n    },\n    \".ada\": {\n        \"color\": \"#02f88c\",\n        \"name\": \"Ada\"\n    },\n    \".adb\": {\n        \"color\": \"#02f88c\",\n        \"name\": \"Ada\"\n    },\n    \".ado\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".adp\": {\n        \"color\": \"#e4cc98\",\n        \"name\": \"Tcl\"\n    },\n    \".ads\": {\n        \"color\": \"#02f88c\",\n        \"name\": \"Ada\"\n    },\n    \".agc\": {\n        \"color\": \"#0B3D91\",\n        \"name\": \"Apollo Guidance Computer\"\n    },\n    \".agda\": {\n        \"color\": \"#315665\",\n        \"name\": \"Agda\"\n    },\n    \".ahk\": {\n        \"color\": \"#6594b9\",\n        \"name\": \"AutoHotkey\"\n    },\n    \".ahkl\": {\n        \"color\": \"#6594b9\",\n        \"name\": \"AutoHotkey\"\n    },\n    \".aidl\": {\n        \"color\": \"#34EB6B\",\n        \"name\": \"AIDL\"\n    },\n    \".aj\": {\n        \"color\": \"#a957b0\",\n        \"name\": \"AspectJ\"\n    },\n    \".al\": {\n        \"color\": \"#0298c3\",\n        \"name\": \"Perl\"\n    },\n    \".als\": {\n        \"color\": \"#64C800\",\n        \"name\": \"Alloy\"\n    },\n    \".ampl\": {\n        \"color\": \"#E6EFBB\",\n        \"name\": \"AMPL\"\n    },\n    \".angelscript\": {\n        \"color\": \"#C7D7DC\",\n        \"name\": \"AngelScript\"\n    },\n    \".apib\": {\n        \"color\": \"#2ACCA8\",\n        \"name\": \"API Blueprint\"\n    },\n    \".apl\": {\n        \"color\": \"#5A8164\",\n        \"name\": \"APL\"\n    },\n    \".app.src\": {\n        \"color\": \"#B83998\",\n        \"name\": \"Erlang\"\n    },\n    \".applescript\": {\n        \"color\": \"#101F1F\",\n        \"name\": \"AppleScript\"\n    },\n    \".arc\": {\n        \"color\": \"#aa2afe\",\n        \"name\": \"Arc\"\n    },\n    \".as\": {\n        \"color\": \"#C7D7DC\",\n        \"name\": \"AngelScript\"\n    },\n    \".asax\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"ASP.NET\"\n    },\n    \".asc\": {\n        \"color\": \"#B9D9FF\",\n        \"name\": \"AGS Script\"\n    },\n    \".ascx\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"ASP.NET\"\n    },\n    \".asd\": {\n        \"color\": \"#3fb68b\",\n        \"name\": \"Common Lisp\"\n    },\n    \".asddls\": {\n        \"color\": \"#555e25\",\n        \"name\": \"ABAP CDS\"\n    },\n    \".ash\": {\n        \"color\": \"#B9D9FF\",\n        \"name\": \"AGS Script\"\n    },\n    \".ashx\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"ASP.NET\"\n    },\n    \".asm\": {\n        \"color\": \"#005daa\",\n        \"name\": \"Motorola 68K Assembly\"\n    },\n    \".asmx\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"ASP.NET\"\n    },\n    \".asp\": {\n        \"color\": \"#6a40fd\",\n        \"name\": \"Classic ASP\"\n    },\n    \".aspx\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"ASP.NET\"\n    },\n    \".astro\": {\n        \"color\": \"#ff5a03\",\n        \"name\": \"Astro\"\n    },\n    \".asy\": {\n        \"color\": \"#ff0000\",\n        \"name\": \"Asymptote\"\n    },\n    \".au3\": {\n        \"color\": \"#1C3552\",\n        \"name\": \"AutoIt\"\n    },\n    \".aug\": {\n        \"color\": \"#9CC134\",\n        \"name\": \"Augeas\"\n    },\n    \".auk\": {\n        \"color\": \"#c30e9b\",\n        \"name\": \"Awk\"\n    },\n    \".aux\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".aw\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".awk\": {\n        \"color\": \"#c30e9b\",\n        \"name\": \"Awk\"\n    },\n    \".axd\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"ASP.NET\"\n    },\n    \".axi\": {\n        \"color\": \"#0aa0ff\",\n        \"name\": \"NetLinx\"\n    },\n    \".axi.erb\": {\n        \"color\": \"#747faa\",\n        \"name\": \"NetLinx+ERB\"\n    },\n    \".axs\": {\n        \"color\": \"#0aa0ff\",\n        \"name\": \"NetLinx\"\n    },\n    \".axs.erb\": {\n        \"color\": \"#747faa\",\n        \"name\": \"NetLinx+ERB\"\n    },\n    \".b\": {\n        \"color\": \"#2F2530\",\n        \"name\": \"Brainfuck\"\n    },\n    \".bal\": {\n        \"color\": \"#FF5000\",\n        \"name\": \"Ballerina\"\n    },\n    \".bas\": {\n        \"color\": \"#867db1\",\n        \"name\": \"VBA\"\n    },\n    \".bash\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".bat\": {\n        \"color\": \"#C1F12E\",\n        \"name\": \"Batchfile\"\n    },\n    \".bats\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".bb\": {\n        \"color\": \"#00FFAE\",\n        \"name\": \"BlitzBasic\"\n    },\n    \".bbx\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".bdy\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".bf\": {\n        \"color\": \"#2F2530\",\n        \"name\": \"Brainfuck\"\n    },\n    \".bi\": {\n        \"color\": \"#867db1\",\n        \"name\": \"FreeBasic\"\n    },\n    \".bib\": {\n        \"color\": \"#778899\",\n        \"name\": \"BibTeX\"\n    },\n    \".bibtex\": {\n        \"color\": \"#778899\",\n        \"name\": \"BibTeX\"\n    },\n    \".bicep\": {\n        \"color\": \"#519aba\",\n        \"name\": \"Bicep\"\n    },\n    \".bison\": {\n        \"color\": \"#6A463F\",\n        \"name\": \"Bison\"\n    },\n    \".blade\": {\n        \"color\": \"#f7523f\",\n        \"name\": \"Blade\"\n    },\n    \".blade.php\": {\n        \"color\": \"#f7523f\",\n        \"name\": \"Blade\"\n    },\n    \".bmx\": {\n        \"color\": \"#cd6400\",\n        \"name\": \"BlitzMax\"\n    },\n    \".bones\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".boo\": {\n        \"color\": \"#d4bec1\",\n        \"name\": \"Boo\"\n    },\n    \".boot\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".bpl\": {\n        \"color\": \"#c80fa0\",\n        \"name\": \"Boogie\"\n    },\n    \".brs\": {\n        \"color\": \"#662D91\",\n        \"name\": \"Brightscript\"\n    },\n    \".bsl\": {\n        \"color\": \"#814CCC\",\n        \"name\": \"1C Enterprise\"\n    },\n    \".bsv\": {\n        \"color\": \"#12223c\",\n        \"name\": \"Bluespec\"\n    },\n    \".builder\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".bzl\": {\n        \"color\": \"#76d275\",\n        \"name\": \"Starlark\"\n    },\n    \".c\": {\n        \"color\": \"#555555\",\n        \"name\": \"C\"\n    },\n    \".c++\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".cake\": {\n        \"color\": \"#244776\",\n        \"name\": \"CoffeeScript\"\n    },\n    \".capnp\": {\n        \"color\": \"#c42727\",\n        \"name\": \"Cap'n Proto\"\n    },\n    \".cats\": {\n        \"color\": \"#555555\",\n        \"name\": \"C\"\n    },\n    \".cbx\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".cc\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".cdf\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".ceylon\": {\n        \"color\": \"#dfa535\",\n        \"name\": \"Ceylon\"\n    },\n    \".cfc\": {\n        \"color\": \"#ed2cd6\",\n        \"name\": \"ColdFusion CFC\"\n    },\n    \".cfm\": {\n        \"color\": \"#ed2cd6\",\n        \"name\": \"ColdFusion\"\n    },\n    \".cfml\": {\n        \"color\": \"#ed2cd6\",\n        \"name\": \"ColdFusion\"\n    },\n    \".cgi\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".cginc\": {\n        \"color\": \"#aace60\",\n        \"name\": \"HLSL\"\n    },\n    \".ch\": {\n        \"color\": \"#403a40\",\n        \"name\": \"xBase\"\n    },\n    \".chpl\": {\n        \"color\": \"#8dc63f\",\n        \"name\": \"Chapel\"\n    },\n    \".cirru\": {\n        \"color\": \"#ccccff\",\n        \"name\": \"Cirru\"\n    },\n    \".cjs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".cjsx\": {\n        \"color\": \"#244776\",\n        \"name\": \"CoffeeScript\"\n    },\n    \".ck\": {\n        \"color\": \"#3f8000\",\n        \"name\": \"ChucK\"\n    },\n    \".cl\": {\n        \"color\": \"#ed2e2d\",\n        \"name\": \"OpenCL\"\n    },\n    \".cl2\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".click\": {\n        \"color\": \"#E4E6F3\",\n        \"name\": \"Click\"\n    },\n    \".clj\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".cljc\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".cljs\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".cljs.hl\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".cljscm\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".cljx\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".clp\": {\n        \"color\": \"#00A300\",\n        \"name\": \"CLIPS\"\n    },\n    \".cls\": {\n        \"color\": \"#867db1\",\n        \"name\": \"VBA\"\n    },\n    \".clw\": {\n        \"color\": \"#db901e\",\n        \"name\": \"Clarion\"\n    },\n    \".cmake\": {\n        \"color\": \"#DA3434\",\n        \"name\": \"CMake\"\n    },\n    \".cmake.in\": {\n        \"color\": \"#DA3434\",\n        \"name\": \"CMake\"\n    },\n    \".cmd\": {\n        \"color\": \"#C1F12E\",\n        \"name\": \"Batchfile\"\n    },\n    \".cnc\": {\n        \"color\": \"#D08CF2\",\n        \"name\": \"G-code\"\n    },\n    \".cocci\": {\n        \"color\": \"#c94949\",\n        \"name\": \"SmPL\"\n    },\n    \".coffee\": {\n        \"color\": \"#244776\",\n        \"name\": \"CoffeeScript\"\n    },\n    \".coffee.md\": {\n        \"color\": \"#244776\",\n        \"name\": \"Literate CoffeeScript\"\n    },\n    \".command\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".coq\": {\n        \"color\": \"#d0b68c\",\n        \"name\": \"Coq\"\n    },\n    \".cp\": {\n        \"color\": \"#B0CE4E\",\n        \"name\": \"Component Pascal\"\n    },\n    \".cpp\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".cps\": {\n        \"color\": \"#B0CE4E\",\n        \"name\": \"Component Pascal\"\n    },\n    \".cr\": {\n        \"color\": \"#000100\",\n        \"name\": \"Crystal\"\n    },\n    \".cs\": {\n        \"color\": \"#178600\",\n        \"name\": \"C#\"\n    },\n    \".csd\": {\n        \"color\": \"#1a1a1a\",\n        \"name\": \"Csound Document\"\n    },\n    \".cshtml\": {\n        \"color\": \"#512be4\",\n        \"name\": \"HTML+Razor\"\n    },\n    \".css\": {\n        \"color\": \"#563d7c\",\n        \"name\": \"CSS\"\n    },\n    \".csx\": {\n        \"color\": \"#178600\",\n        \"name\": \"C#\"\n    },\n    \".ctp\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".cu\": {\n        \"color\": \"#3A4E3A\",\n        \"name\": \"Cuda\"\n    },\n    \".cue\": {\n        \"color\": \"#5886E1\",\n        \"name\": \"CUE\"\n    },\n    \".cuh\": {\n        \"color\": \"#3A4E3A\",\n        \"name\": \"Cuda\"\n    },\n    \".cwl\": {\n        \"color\": \"#B5314C\",\n        \"name\": \"Common Workflow Language\"\n    },\n    \".cxx\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".d\": {\n        \"color\": \"#427819\",\n        \"name\": \"Makefile\"\n    },\n    \".dart\": {\n        \"color\": \"#00B4AB\",\n        \"name\": \"Dart\"\n    },\n    \".dats\": {\n        \"color\": \"#1ac620\",\n        \"name\": \"ATS\"\n    },\n    \".db2\": {\n        \"color\": \"#e38c00\",\n        \"name\": \"SQLPL\"\n    },\n    \".dcl\": {\n        \"color\": \"#3F85AF\",\n        \"name\": \"Clean\"\n    },\n    \".ddl\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".decls\": {\n        \"color\": \"#00FFAE\",\n        \"name\": \"BlitzBasic\"\n    },\n    \".dfm\": {\n        \"color\": \"#E3F171\",\n        \"name\": \"Pascal\"\n    },\n    \".dfy\": {\n        \"color\": \"#FFEC25\",\n        \"name\": \"Dafny\"\n    },\n    \".dhall\": {\n        \"color\": \"#dfafff\",\n        \"name\": \"Dhall\"\n    },\n    \".di\": {\n        \"color\": \"#ba595e\",\n        \"name\": \"D\"\n    },\n    \".djs\": {\n        \"color\": \"#cca760\",\n        \"name\": \"Dogescript\"\n    },\n    \".dlm\": {\n        \"color\": \"#a3522f\",\n        \"name\": \"IDL\"\n    },\n    \".dm\": {\n        \"color\": \"#447265\",\n        \"name\": \"DM\"\n    },\n    \".do\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".dockerfile\": {\n        \"color\": \"#384d54\",\n        \"name\": \"Dockerfile\"\n    },\n    \".doh\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".dpr\": {\n        \"color\": \"#E3F171\",\n        \"name\": \"Pascal\"\n    },\n    \".druby\": {\n        \"color\": \"#c7a938\",\n        \"name\": \"Mirah\"\n    },\n    \".dsp\": {\n        \"color\": \"#c37240\",\n        \"name\": \"Faust\"\n    },\n    \".dtx\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".duby\": {\n        \"color\": \"#c7a938\",\n        \"name\": \"Mirah\"\n    },\n    \".dwl\": {\n        \"color\": \"#003a52\",\n        \"name\": \"DataWeave\"\n    },\n    \".dyalog\": {\n        \"color\": \"#5A8164\",\n        \"name\": \"APL\"\n    },\n    \".dyl\": {\n        \"color\": \"#6c616e\",\n        \"name\": \"Dylan\"\n    },\n    \".dylan\": {\n        \"color\": \"#6c616e\",\n        \"name\": \"Dylan\"\n    },\n    \".e\": {\n        \"color\": \"#4d6977\",\n        \"name\": \"Eiffel\"\n    },\n    \".ebuild\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"Gentoo Ebuild\"\n    },\n    \".ec\": {\n        \"color\": \"#913960\",\n        \"name\": \"eC\"\n    },\n    \".ecl\": {\n        \"color\": \"#001d9d\",\n        \"name\": \"ECLiPSe\"\n    },\n    \".eclass\": {\n        \"color\": \"#9400ff\",\n        \"name\": \"Gentoo Eclass\"\n    },\n    \".eclxml\": {\n        \"color\": \"#8a1267\",\n        \"name\": \"ECL\"\n    },\n    \".ecr\": {\n        \"color\": \"#2e1052\",\n        \"name\": \"HTML+ECR\"\n    },\n    \".ect\": {\n        \"color\": \"#a91e50\",\n        \"name\": \"EJS\"\n    },\n    \".eex\": {\n        \"color\": \"#6e4a7e\",\n        \"name\": \"HTML+EEX\"\n    },\n    \".eh\": {\n        \"color\": \"#913960\",\n        \"name\": \"eC\"\n    },\n    \".ejs\": {\n        \"color\": \"#a91e50\",\n        \"name\": \"EJS\"\n    },\n    \".el\": {\n        \"color\": \"#c065db\",\n        \"name\": \"Emacs Lisp\"\n    },\n    \".eliom\": {\n        \"color\": \"#3be133\",\n        \"name\": \"OCaml\"\n    },\n    \".eliomi\": {\n        \"color\": \"#3be133\",\n        \"name\": \"OCaml\"\n    },\n    \".elm\": {\n        \"color\": \"#60B5CC\",\n        \"name\": \"Elm\"\n    },\n    \".em\": {\n        \"color\": \"#FFF4F3\",\n        \"name\": \"EmberScript\"\n    },\n    \".emacs\": {\n        \"color\": \"#c065db\",\n        \"name\": \"Emacs Lisp\"\n    },\n    \".emacs.desktop\": {\n        \"color\": \"#c065db\",\n        \"name\": \"Emacs Lisp\"\n    },\n    \".emberscript\": {\n        \"color\": \"#FFF4F3\",\n        \"name\": \"EmberScript\"\n    },\n    \".env\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".eps\": {\n        \"color\": \"#da291c\",\n        \"name\": \"PostScript\"\n    },\n    \".epsi\": {\n        \"color\": \"#da291c\",\n        \"name\": \"PostScript\"\n    },\n    \".eq\": {\n        \"color\": \"#a78649\",\n        \"name\": \"EQ\"\n    },\n    \".erb\": {\n        \"color\": \"#701516\",\n        \"name\": \"HTML+ERB\"\n    },\n    \".erb.deface\": {\n        \"color\": \"#701516\",\n        \"name\": \"HTML+ERB\"\n    },\n    \".erl\": {\n        \"color\": \"#B83998\",\n        \"name\": \"Erlang\"\n    },\n    \".es\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".es6\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".escript\": {\n        \"color\": \"#B83998\",\n        \"name\": \"Erlang\"\n    },\n    \".ex\": {\n        \"color\": \"#6e4a7e\",\n        \"name\": \"Elixir\"\n    },\n    \".exs\": {\n        \"color\": \"#6e4a7e\",\n        \"name\": \"Elixir\"\n    },\n    \".eye\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".f\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran\"\n    },\n    \".f03\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran Free Form\"\n    },\n    \".f08\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran Free Form\"\n    },\n    \".f77\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran\"\n    },\n    \".f90\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran Free Form\"\n    },\n    \".f95\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran Free Form\"\n    },\n    \".factor\": {\n        \"color\": \"#636746\",\n        \"name\": \"Factor\"\n    },\n    \".fan\": {\n        \"color\": \"#14253c\",\n        \"name\": \"Fantom\"\n    },\n    \".fancypack\": {\n        \"color\": \"#7b9db4\",\n        \"name\": \"Fancy\"\n    },\n    \".fcgi\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".feature\": {\n        \"color\": \"#5B2063\",\n        \"name\": \"Gherkin\"\n    },\n    \".fish\": {\n        \"color\": \"#4aae47\",\n        \"name\": \"fish\"\n    },\n    \".flex\": {\n        \"color\": \"#DBCA00\",\n        \"name\": \"JFlex\"\n    },\n    \".flux\": {\n        \"color\": \"#88ccff\",\n        \"name\": \"FLUX\"\n    },\n    \".fnc\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".fnl\": {\n        \"color\": \"#fff3d7\",\n        \"name\": \"Fennel\"\n    },\n    \".for\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran\"\n    },\n    \".forth\": {\n        \"color\": \"#341708\",\n        \"name\": \"Forth\"\n    },\n    \".fp\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".fpp\": {\n        \"color\": \"#4d41b1\",\n        \"name\": \"Fortran\"\n    },\n    \".fr\": {\n        \"color\": \"#00cafe\",\n        \"name\": \"Frege\"\n    },\n    \".frag\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".frg\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".frm\": {\n        \"color\": \"#867db1\",\n        \"name\": \"VBA\"\n    },\n    \".frt\": {\n        \"color\": \"#341708\",\n        \"name\": \"Forth\"\n    },\n    \".frx\": {\n        \"color\": \"#867db1\",\n        \"name\": \"VBA\"\n    },\n    \".fs\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".fsh\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".fshader\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".fsi\": {\n        \"color\": \"#b845fc\",\n        \"name\": \"F#\"\n    },\n    \".fst\": {\n        \"color\": \"#572e30\",\n        \"name\": \"F*\"\n    },\n    \".fsx\": {\n        \"color\": \"#b845fc\",\n        \"name\": \"F#\"\n    },\n    \".fth\": {\n        \"color\": \"#341708\",\n        \"name\": \"Forth\"\n    },\n    \".ftl\": {\n        \"color\": \"#0050b2\",\n        \"name\": \"FreeMarker\"\n    },\n    \".fun\": {\n        \"color\": \"#dc566d\",\n        \"name\": \"Standard ML\"\n    },\n    \".fut\": {\n        \"color\": \"#5f021f\",\n        \"name\": \"Futhark\"\n    },\n    \".fx\": {\n        \"color\": \"#aace60\",\n        \"name\": \"HLSL\"\n    },\n    \".fxh\": {\n        \"color\": \"#aace60\",\n        \"name\": \"HLSL\"\n    },\n    \".fy\": {\n        \"color\": \"#7b9db4\",\n        \"name\": \"Fancy\"\n    },\n    \".g\": {\n        \"color\": \"#0000cc\",\n        \"name\": \"GAP\"\n    },\n    \".g4\": {\n        \"color\": \"#9DC3FF\",\n        \"name\": \"ANTLR\"\n    },\n    \".gaml\": {\n        \"color\": \"#FFC766\",\n        \"name\": \"GAML\"\n    },\n    \".gap\": {\n        \"color\": \"#0000cc\",\n        \"name\": \"GAP\"\n    },\n    \".gawk\": {\n        \"color\": \"#c30e9b\",\n        \"name\": \"Awk\"\n    },\n    \".gco\": {\n        \"color\": \"#D08CF2\",\n        \"name\": \"G-code\"\n    },\n    \".gcode\": {\n        \"color\": \"#D08CF2\",\n        \"name\": \"G-code\"\n    },\n    \".gd\": {\n        \"color\": \"#355570\",\n        \"name\": \"GDScript\"\n    },\n    \".gemspec\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".geo\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".geom\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".gf\": {\n        \"color\": \"#ff0000\",\n        \"name\": \"Grammatical Framework\"\n    },\n    \".gi\": {\n        \"color\": \"#0000cc\",\n        \"name\": \"GAP\"\n    },\n    \".glf\": {\n        \"color\": \"#c1ac7f\",\n        \"name\": \"Glyph\"\n    },\n    \".glsl\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".glslf\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".glslv\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".gml\": {\n        \"color\": \"#71b417\",\n        \"name\": \"Game Maker Language\"\n    },\n    \".gms\": {\n        \"color\": \"#f49a22\",\n        \"name\": \"GAMS\"\n    },\n    \".gnu\": {\n        \"color\": \"#f0a9f0\",\n        \"name\": \"Gnuplot\"\n    },\n    \".gnuplot\": {\n        \"color\": \"#f0a9f0\",\n        \"name\": \"Gnuplot\"\n    },\n    \".go\": {\n        \"color\": \"#00ADD8\",\n        \"name\": \"Go\"\n    },\n    \".god\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".golo\": {\n        \"color\": \"#88562A\",\n        \"name\": \"Golo\"\n    },\n    \".gp\": {\n        \"color\": \"#f0a9f0\",\n        \"name\": \"Gnuplot\"\n    },\n    \".grace\": {\n        \"color\": \"#615f8b\",\n        \"name\": \"Grace\"\n    },\n    \".groovy\": {\n        \"color\": \"#4298b8\",\n        \"name\": \"Groovy\"\n    },\n    \".grt\": {\n        \"color\": \"#4298b8\",\n        \"name\": \"Groovy\"\n    },\n    \".gs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".gshader\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".gsp\": {\n        \"color\": \"#4298b8\",\n        \"name\": \"Groovy Server Pages\"\n    },\n    \".gst\": {\n        \"color\": \"#82937f\",\n        \"name\": \"Gosu\"\n    },\n    \".gsx\": {\n        \"color\": \"#82937f\",\n        \"name\": \"Gosu\"\n    },\n    \".gtpl\": {\n        \"color\": \"#4298b8\",\n        \"name\": \"Groovy\"\n    },\n    \".gvy\": {\n        \"color\": \"#4298b8\",\n        \"name\": \"Groovy\"\n    },\n    \".gyp\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".gypi\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".h\": {\n        \"color\": \"#438eff\",\n        \"name\": \"Objective-C\"\n    },\n    \".h++\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".hack\": {\n        \"color\": \"#878787\",\n        \"name\": \"Hack\"\n    },\n    \".haml\": {\n        \"color\": \"#ece2a9\",\n        \"name\": \"Haml\"\n    },\n    \".haml.deface\": {\n        \"color\": \"#ece2a9\",\n        \"name\": \"Haml\"\n    },\n    \".handlebars\": {\n        \"color\": \"#f7931e\",\n        \"name\": \"Handlebars\"\n    },\n    \".hats\": {\n        \"color\": \"#1ac620\",\n        \"name\": \"ATS\"\n    },\n    \".hb\": {\n        \"color\": \"#0e60e3\",\n        \"name\": \"Harbour\"\n    },\n    \".hbs\": {\n        \"color\": \"#f7931e\",\n        \"name\": \"Handlebars\"\n    },\n    \".hc\": {\n        \"color\": \"#ffefaf\",\n        \"name\": \"HolyC\"\n    },\n    \".hh\": {\n        \"color\": \"#878787\",\n        \"name\": \"Hack\"\n    },\n    \".hhi\": {\n        \"color\": \"#878787\",\n        \"name\": \"Hack\"\n    },\n    \".hic\": {\n        \"color\": \"#db5855\",\n        \"name\": \"Clojure\"\n    },\n    \".hlsl\": {\n        \"color\": \"#aace60\",\n        \"name\": \"HLSL\"\n    },\n    \".hlsli\": {\n        \"color\": \"#aace60\",\n        \"name\": \"HLSL\"\n    },\n    \".hpp\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".hqf\": {\n        \"color\": \"#3F3F3F\",\n        \"name\": \"SQF\"\n    },\n    \".hql\": {\n        \"color\": \"#dce200\",\n        \"name\": \"HiveQL\"\n    },\n    \".hrl\": {\n        \"color\": \"#B83998\",\n        \"name\": \"Erlang\"\n    },\n    \".hs\": {\n        \"color\": \"#5e5086\",\n        \"name\": \"Haskell\"\n    },\n    \".hs-boot\": {\n        \"color\": \"#5e5086\",\n        \"name\": \"Haskell\"\n    },\n    \".hsc\": {\n        \"color\": \"#5e5086\",\n        \"name\": \"Haskell\"\n    },\n    \".hta\": {\n        \"color\": \"#e34c26\",\n        \"name\": \"HTML\"\n    },\n    \".htm\": {\n        \"color\": \"#e34c26\",\n        \"name\": \"HTML\"\n    },\n    \".html\": {\n        \"color\": \"#e34c26\",\n        \"name\": \"HTML\"\n    },\n    \".html.hl\": {\n        \"color\": \"#e34c26\",\n        \"name\": \"HTML\"\n    },\n    \".html.leex\": {\n        \"color\": \"#6e4a7e\",\n        \"name\": \"HTML+EEX\"\n    },\n    \".hx\": {\n        \"color\": \"#df7900\",\n        \"name\": \"Haxe\"\n    },\n    \".hxsl\": {\n        \"color\": \"#df7900\",\n        \"name\": \"Haxe\"\n    },\n    \".hxx\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".hy\": {\n        \"color\": \"#7790B2\",\n        \"name\": \"Hy\"\n    },\n    \".i\": {\n        \"color\": \"#005daa\",\n        \"name\": \"Motorola 68K Assembly\"\n    },\n    \".i3\": {\n        \"color\": \"#223388\",\n        \"name\": \"Modula-3\"\n    },\n    \".ice\": {\n        \"color\": \"#003fa2\",\n        \"name\": \"Slice\"\n    },\n    \".iced\": {\n        \"color\": \"#244776\",\n        \"name\": \"CoffeeScript\"\n    },\n    \".icl\": {\n        \"color\": \"#3F85AF\",\n        \"name\": \"Clean\"\n    },\n    \".idc\": {\n        \"color\": \"#555555\",\n        \"name\": \"C\"\n    },\n    \".idr\": {\n        \"color\": \"#b30000\",\n        \"name\": \"Idris\"\n    },\n    \".ig\": {\n        \"color\": \"#223388\",\n        \"name\": \"Modula-3\"\n    },\n    \".ihlp\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".ijm\": {\n        \"color\": \"#99AAFF\",\n        \"name\": \"ImageJ Macro\"\n    },\n    \".ijs\": {\n        \"color\": \"#9EEDFF\",\n        \"name\": \"J\"\n    },\n    \".ik\": {\n        \"color\": \"#078193\",\n        \"name\": \"Ioke\"\n    },\n    \".ily\": {\n        \"color\": \"#9ccc7c\",\n        \"name\": \"LilyPond\"\n    },\n    \".inc\": {\n        \"color\": \"#f69e1d\",\n        \"name\": \"SourcePawn\"\n    },\n    \".inl\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".ino\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".ins\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".intr\": {\n        \"color\": \"#6c616e\",\n        \"name\": \"Dylan\"\n    },\n    \".io\": {\n        \"color\": \"#a9188d\",\n        \"name\": \"Io\"\n    },\n    \".iol\": {\n        \"color\": \"#843179\",\n        \"name\": \"Jolie\"\n    },\n    \".ipf\": {\n        \"color\": \"#0000cc\",\n        \"name\": \"IGOR Pro\"\n    },\n    \".ipp\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".ipynb\": {\n        \"color\": \"#DA5B0B\",\n        \"name\": \"Jupyter Notebook\"\n    },\n    \".isl\": {\n        \"color\": \"#264b99\",\n        \"name\": \"Inno Setup\"\n    },\n    \".iss\": {\n        \"color\": \"#264b99\",\n        \"name\": \"Inno Setup\"\n    },\n    \".j\": {\n        \"color\": \"#ff0c5a\",\n        \"name\": \"Objective-J\"\n    },\n    \".j2\": {\n        \"color\": \"#a52a22\",\n        \"name\": \"Jinja\"\n    },\n    \".jade\": {\n        \"color\": \"#a86454\",\n        \"name\": \"Pug\"\n    },\n    \".jake\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jav\": {\n        \"color\": \"#b07219\",\n        \"name\": \"Java\"\n    },\n    \".java\": {\n        \"color\": \"#b07219\",\n        \"name\": \"Java\"\n    },\n    \".javascript\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jbuilder\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".jflex\": {\n        \"color\": \"#DBCA00\",\n        \"name\": \"JFlex\"\n    },\n    \".jinja\": {\n        \"color\": \"#a52a22\",\n        \"name\": \"Jinja\"\n    },\n    \".jinja2\": {\n        \"color\": \"#a52a22\",\n        \"name\": \"Jinja\"\n    },\n    \".jison\": {\n        \"color\": \"#56b3cb\",\n        \"name\": \"Jison\"\n    },\n    \".jisonlex\": {\n        \"color\": \"#56b3cb\",\n        \"name\": \"Jison Lex\"\n    },\n    \".jl\": {\n        \"color\": \"#a270ba\",\n        \"name\": \"Julia\"\n    },\n    \".jq\": {\n        \"color\": \"#c7254e\",\n        \"name\": \"jq\"\n    },\n    \".js\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".js.erb\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript+ERB\"\n    },\n    \".jsb\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jscad\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jsfl\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jsm\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jsonnet\": {\n        \"color\": \"#0064bd\",\n        \"name\": \"Jsonnet\"\n    },\n    \".jsp\": {\n        \"color\": \"#2A6277\",\n        \"name\": \"Java Server Pages\"\n    },\n    \".jss\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".jst\": {\n        \"color\": \"#a91e50\",\n        \"name\": \"EJS\"\n    },\n    \".jsx\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".kak\": {\n        \"color\": \"#6f8042\",\n        \"name\": \"KakouneScript\"\n    },\n    \".kid\": {\n        \"color\": \"#951531\",\n        \"name\": \"Genshi\"\n    },\n    \".kojo\": {\n        \"color\": \"#c22d40\",\n        \"name\": \"Scala\"\n    },\n    \".krl\": {\n        \"color\": \"#28430A\",\n        \"name\": \"KRL\"\n    },\n    \".ksh\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".ksy\": {\n        \"color\": \"#773b37\",\n        \"name\": \"Kaitai Struct\"\n    },\n    \".kt\": {\n        \"color\": \"#A97BFF\",\n        \"name\": \"Kotlin\"\n    },\n    \".ktm\": {\n        \"color\": \"#A97BFF\",\n        \"name\": \"Kotlin\"\n    },\n    \".kts\": {\n        \"color\": \"#A97BFF\",\n        \"name\": \"Kotlin\"\n    },\n    \".l\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".lagda\": {\n        \"color\": \"#315665\",\n        \"name\": \"Literate Agda\"\n    },\n    \".las\": {\n        \"color\": \"#999999\",\n        \"name\": \"Lasso\"\n    },\n    \".lasso\": {\n        \"color\": \"#999999\",\n        \"name\": \"Lasso\"\n    },\n    \".lasso8\": {\n        \"color\": \"#999999\",\n        \"name\": \"Lasso\"\n    },\n    \".lasso9\": {\n        \"color\": \"#999999\",\n        \"name\": \"Lasso\"\n    },\n    \".latte\": {\n        \"color\": \"#f2a542\",\n        \"name\": \"Latte\"\n    },\n    \".lbx\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".less\": {\n        \"color\": \"#1d365d\",\n        \"name\": \"Less\"\n    },\n    \".lex\": {\n        \"color\": \"#DBCA00\",\n        \"name\": \"Lex\"\n    },\n    \".lfe\": {\n        \"color\": \"#4C3023\",\n        \"name\": \"LFE\"\n    },\n    \".lgt\": {\n        \"color\": \"#295b9a\",\n        \"name\": \"Logtalk\"\n    },\n    \".lhs\": {\n        \"color\": \"#5e5086\",\n        \"name\": \"Literate Haskell\"\n    },\n    \".libsonnet\": {\n        \"color\": \"#0064bd\",\n        \"name\": \"Jsonnet\"\n    },\n    \".lid\": {\n        \"color\": \"#6c616e\",\n        \"name\": \"Dylan\"\n    },\n    \".lidr\": {\n        \"color\": \"#b30000\",\n        \"name\": \"Idris\"\n    },\n    \".linq\": {\n        \"color\": \"#178600\",\n        \"name\": \"C#\"\n    },\n    \".liquid\": {\n        \"color\": \"#67b8de\",\n        \"name\": \"Liquid\"\n    },\n    \".lisp\": {\n        \"color\": \"#87AED7\",\n        \"name\": \"NewLisp\"\n    },\n    \".litcoffee\": {\n        \"color\": \"#244776\",\n        \"name\": \"Literate CoffeeScript\"\n    },\n    \".ll\": {\n        \"color\": \"#185619\",\n        \"name\": \"LLVM\"\n    },\n    \".lmi\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".logtalk\": {\n        \"color\": \"#295b9a\",\n        \"name\": \"Logtalk\"\n    },\n    \".lol\": {\n        \"color\": \"#cc9900\",\n        \"name\": \"LOLCODE\"\n    },\n    \".lookml\": {\n        \"color\": \"#652B81\",\n        \"name\": \"LookML\"\n    },\n    \".lpr\": {\n        \"color\": \"#E3F171\",\n        \"name\": \"Pascal\"\n    },\n    \".ls\": {\n        \"color\": \"#499886\",\n        \"name\": \"LiveScript\"\n    },\n    \".lsl\": {\n        \"color\": \"#3d9970\",\n        \"name\": \"LSL\"\n    },\n    \".lslp\": {\n        \"color\": \"#3d9970\",\n        \"name\": \"LSL\"\n    },\n    \".lsp\": {\n        \"color\": \"#87AED7\",\n        \"name\": \"NewLisp\"\n    },\n    \".ltx\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".lua\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".lvlib\": {\n        \"color\": \"#fede06\",\n        \"name\": \"LabVIEW\"\n    },\n    \".lvproj\": {\n        \"color\": \"#fede06\",\n        \"name\": \"LabVIEW\"\n    },\n    \".ly\": {\n        \"color\": \"#9ccc7c\",\n        \"name\": \"LilyPond\"\n    },\n    \".m\": {\n        \"color\": \"#438eff\",\n        \"name\": \"Objective-C\"\n    },\n    \".m2\": {\n        \"color\": \"#d8ffff\",\n        \"name\": \"Macaulay2\"\n    },\n    \".m3\": {\n        \"color\": \"#223388\",\n        \"name\": \"Modula-3\"\n    },\n    \".ma\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".mak\": {\n        \"color\": \"#427819\",\n        \"name\": \"Makefile\"\n    },\n    \".make\": {\n        \"color\": \"#427819\",\n        \"name\": \"Makefile\"\n    },\n    \".makefile\": {\n        \"color\": \"#427819\",\n        \"name\": \"Makefile\"\n    },\n    \".mako\": {\n        \"color\": \"#7e858d\",\n        \"name\": \"Mako\"\n    },\n    \".man\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".mao\": {\n        \"color\": \"#7e858d\",\n        \"name\": \"Mako\"\n    },\n    \".marko\": {\n        \"color\": \"#42bff2\",\n        \"name\": \"Marko\"\n    },\n    \".mask\": {\n        \"color\": \"#f97732\",\n        \"name\": \"Mask\"\n    },\n    \".mata\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".matah\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".mathematica\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".matlab\": {\n        \"color\": \"#e16737\",\n        \"name\": \"MATLAB\"\n    },\n    \".mawk\": {\n        \"color\": \"#c30e9b\",\n        \"name\": \"Awk\"\n    },\n    \".maxhelp\": {\n        \"color\": \"#c4a79c\",\n        \"name\": \"Max\"\n    },\n    \".maxpat\": {\n        \"color\": \"#c4a79c\",\n        \"name\": \"Max\"\n    },\n    \".maxproj\": {\n        \"color\": \"#c4a79c\",\n        \"name\": \"Max\"\n    },\n    \".mcfunction\": {\n        \"color\": \"#E22837\",\n        \"name\": \"mcfunction\"\n    },\n    \".mcr\": {\n        \"color\": \"#00a6a6\",\n        \"name\": \"MAXScript\"\n    },\n    \".mdoc\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff Manpage\"\n    },\n    \".me\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".metal\": {\n        \"color\": \"#8f14e9\",\n        \"name\": \"Metal\"\n    },\n    \".mg\": {\n        \"color\": \"#223388\",\n        \"name\": \"Modula-3\"\n    },\n    \".mirah\": {\n        \"color\": \"#c7a938\",\n        \"name\": \"Mirah\"\n    },\n    \".mjs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".mk\": {\n        \"color\": \"#427819\",\n        \"name\": \"Makefile\"\n    },\n    \".mkfile\": {\n        \"color\": \"#427819\",\n        \"name\": \"Makefile\"\n    },\n    \".mkii\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".mkiv\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".mkvi\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".ml\": {\n        \"color\": \"#dc566d\",\n        \"name\": \"Standard ML\"\n    },\n    \".ml4\": {\n        \"color\": \"#3be133\",\n        \"name\": \"OCaml\"\n    },\n    \".mli\": {\n        \"color\": \"#3be133\",\n        \"name\": \"OCaml\"\n    },\n    \".mlir\": {\n        \"color\": \"#5EC8DB\",\n        \"name\": \"MLIR\"\n    },\n    \".mll\": {\n        \"color\": \"#3be133\",\n        \"name\": \"OCaml\"\n    },\n    \".mly\": {\n        \"color\": \"#3be133\",\n        \"name\": \"OCaml\"\n    },\n    \".mm\": {\n        \"color\": \"#6866fb\",\n        \"name\": \"Objective-C++\"\n    },\n    \".mo\": {\n        \"color\": \"#de1d31\",\n        \"name\": \"Modelica\"\n    },\n    \".mod\": {\n        \"color\": \"#10253f\",\n        \"name\": \"Modula-2\"\n    },\n    \".model.lkml\": {\n        \"color\": \"#652B81\",\n        \"name\": \"LookML\"\n    },\n    \".moo\": {\n        \"color\": \"#ff2b2b\",\n        \"name\": \"Mercury\"\n    },\n    \".moon\": {\n        \"color\": \"#ff4585\",\n        \"name\": \"MoonScript\"\n    },\n    \".mq4\": {\n        \"color\": \"#62A8D6\",\n        \"name\": \"MQL4\"\n    },\n    \".mq5\": {\n        \"color\": \"#4A76B8\",\n        \"name\": \"MQL5\"\n    },\n    \".mqh\": {\n        \"color\": \"#4A76B8\",\n        \"name\": \"MQL5\"\n    },\n    \".mrc\": {\n        \"color\": \"#3d57c3\",\n        \"name\": \"mIRC Script\"\n    },\n    \".ms\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".mspec\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".mt\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".mtml\": {\n        \"color\": \"#b7e1f4\",\n        \"name\": \"MTML\"\n    },\n    \".mu\": {\n        \"color\": \"#244963\",\n        \"name\": \"mupad\"\n    },\n    \".mud\": {\n        \"color\": \"#dc75e5\",\n        \"name\": \"ZIL\"\n    },\n    \".mustache\": {\n        \"color\": \"#724b3b\",\n        \"name\": \"Mustache\"\n    },\n    \".mxt\": {\n        \"color\": \"#c4a79c\",\n        \"name\": \"Max\"\n    },\n    \".n\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".nasm\": {\n        \"color\": \"#6E4C13\",\n        \"name\": \"Assembly\"\n    },\n    \".nawk\": {\n        \"color\": \"#c30e9b\",\n        \"name\": \"Awk\"\n    },\n    \".nb\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".nbp\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".nc\": {\n        \"color\": \"#94B0C7\",\n        \"name\": \"nesC\"\n    },\n    \".ncl\": {\n        \"color\": \"#28431f\",\n        \"name\": \"NCL\"\n    },\n    \".ne\": {\n        \"color\": \"#990000\",\n        \"name\": \"Nearley\"\n    },\n    \".nearley\": {\n        \"color\": \"#990000\",\n        \"name\": \"Nearley\"\n    },\n    \".nf\": {\n        \"color\": \"#3ac486\",\n        \"name\": \"Nextflow\"\n    },\n    \".nim\": {\n        \"color\": \"#ffc200\",\n        \"name\": \"Nim\"\n    },\n    \".nim.cfg\": {\n        \"color\": \"#ffc200\",\n        \"name\": \"Nim\"\n    },\n    \".nimble\": {\n        \"color\": \"#ffc200\",\n        \"name\": \"Nim\"\n    },\n    \".nimrod\": {\n        \"color\": \"#ffc200\",\n        \"name\": \"Nim\"\n    },\n    \".nims\": {\n        \"color\": \"#ffc200\",\n        \"name\": \"Nim\"\n    },\n    \".nit\": {\n        \"color\": \"#009917\",\n        \"name\": \"Nit\"\n    },\n    \".nix\": {\n        \"color\": \"#7e7eff\",\n        \"name\": \"Nix\"\n    },\n    \".njk\": {\n        \"color\": \"#3d8137\",\n        \"name\": \"Nunjucks\"\n    },\n    \".njs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".nl\": {\n        \"color\": \"#87AED7\",\n        \"name\": \"NewLisp\"\n    },\n    \".nlogo\": {\n        \"color\": \"#ff6375\",\n        \"name\": \"NetLogo\"\n    },\n    \".nqp\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".nr\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".nse\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".nss\": {\n        \"color\": \"#111522\",\n        \"name\": \"NWScript\"\n    },\n    \".nu\": {\n        \"color\": \"#c9df40\",\n        \"name\": \"Nu\"\n    },\n    \".numpy\": {\n        \"color\": \"#9C8AF9\",\n        \"name\": \"NumPy\"\n    },\n    \".numpyw\": {\n        \"color\": \"#9C8AF9\",\n        \"name\": \"NumPy\"\n    },\n    \".numsc\": {\n        \"color\": \"#9C8AF9\",\n        \"name\": \"NumPy\"\n    },\n    \".nut\": {\n        \"color\": \"#800000\",\n        \"name\": \"Squirrel\"\n    },\n    \".ny\": {\n        \"color\": \"#3fb68b\",\n        \"name\": \"Common Lisp\"\n    },\n    \".odin\": {\n        \"color\": \"#60AFFE\",\n        \"name\": \"Odin\"\n    },\n    \".ol\": {\n        \"color\": \"#843179\",\n        \"name\": \"Jolie\"\n    },\n    \".omgrofl\": {\n        \"color\": \"#cabbff\",\n        \"name\": \"Omgrofl\"\n    },\n    \".ooc\": {\n        \"color\": \"#b0b77e\",\n        \"name\": \"ooc\"\n    },\n    \".opal\": {\n        \"color\": \"#f7ede0\",\n        \"name\": \"Opal\"\n    },\n    \".opencl\": {\n        \"color\": \"#ed2e2d\",\n        \"name\": \"OpenCL\"\n    },\n    \".orc\": {\n        \"color\": \"#1a1a1a\",\n        \"name\": \"Csound\"\n    },\n    \".os\": {\n        \"color\": \"#814CCC\",\n        \"name\": \"1C Enterprise\"\n    },\n    \".oxygene\": {\n        \"color\": \"#cdd0e3\",\n        \"name\": \"Oxygene\"\n    },\n    \".oz\": {\n        \"color\": \"#fab738\",\n        \"name\": \"Oz\"\n    },\n    \".p\": {\n        \"color\": \"#5ce600\",\n        \"name\": \"OpenEdge ABL\"\n    },\n    \".p4\": {\n        \"color\": \"#7055b5\",\n        \"name\": \"P4\"\n    },\n    \".p6\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".p6l\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".p6m\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".p8\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".pac\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".pan\": {\n        \"color\": \"#cc0000\",\n        \"name\": \"Pan\"\n    },\n    \".parrot\": {\n        \"color\": \"#f3ca0a\",\n        \"name\": \"Parrot\"\n    },\n    \".pas\": {\n        \"color\": \"#E3F171\",\n        \"name\": \"Pascal\"\n    },\n    \".pascal\": {\n        \"color\": \"#E3F171\",\n        \"name\": \"Pascal\"\n    },\n    \".pat\": {\n        \"color\": \"#c4a79c\",\n        \"name\": \"Max\"\n    },\n    \".pb\": {\n        \"color\": \"#5a6986\",\n        \"name\": \"PureBasic\"\n    },\n    \".pbi\": {\n        \"color\": \"#5a6986\",\n        \"name\": \"PureBasic\"\n    },\n    \".pbt\": {\n        \"color\": \"#8f0f8d\",\n        \"name\": \"PowerBuilder\"\n    },\n    \".pck\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".pcss\": {\n        \"color\": \"#dc3a0c\",\n        \"name\": \"PostCSS\"\n    },\n    \".pd_lua\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".pde\": {\n        \"color\": \"#0096D8\",\n        \"name\": \"Processing\"\n    },\n    \".pegjs\": {\n        \"color\": \"#234d6b\",\n        \"name\": \"PEG.js\"\n    },\n    \".pep\": {\n        \"color\": \"#C76F5B\",\n        \"name\": \"Pep8\"\n    },\n    \".perl\": {\n        \"color\": \"#0298c3\",\n        \"name\": \"Perl\"\n    },\n    \".pfa\": {\n        \"color\": \"#da291c\",\n        \"name\": \"PostScript\"\n    },\n    \".pgsql\": {\n        \"color\": \"#336790\",\n        \"name\": \"PLpgSQL\"\n    },\n    \".ph\": {\n        \"color\": \"#0298c3\",\n        \"name\": \"Perl\"\n    },\n    \".php\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".php3\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".php4\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".php5\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".phps\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".phpt\": {\n        \"color\": \"#4F5D95\",\n        \"name\": \"PHP\"\n    },\n    \".phtml\": {\n        \"color\": \"#4f5d95\",\n        \"name\": \"HTML+PHP\"\n    },\n    \".pig\": {\n        \"color\": \"#fcd7de\",\n        \"name\": \"PigLatin\"\n    },\n    \".pike\": {\n        \"color\": \"#005390\",\n        \"name\": \"Pike\"\n    },\n    \".pkb\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".pks\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".pl\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".pl6\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".plb\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".plot\": {\n        \"color\": \"#f0a9f0\",\n        \"name\": \"Gnuplot\"\n    },\n    \".pls\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".plsql\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".plt\": {\n        \"color\": \"#f0a9f0\",\n        \"name\": \"Gnuplot\"\n    },\n    \".pluginspec\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".plx\": {\n        \"color\": \"#0298c3\",\n        \"name\": \"Perl\"\n    },\n    \".pm\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".pm6\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".pmod\": {\n        \"color\": \"#005390\",\n        \"name\": \"Pike\"\n    },\n    \".podsl\": {\n        \"color\": \"#3fb68b\",\n        \"name\": \"Common Lisp\"\n    },\n    \".podspec\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".pogo\": {\n        \"color\": \"#d80074\",\n        \"name\": \"PogoScript\"\n    },\n    \".postcss\": {\n        \"color\": \"#dc3a0c\",\n        \"name\": \"PostCSS\"\n    },\n    \".pov\": {\n        \"color\": \"#6bac65\",\n        \"name\": \"POV-Ray SDL\"\n    },\n    \".pp\": {\n        \"color\": \"#302B6D\",\n        \"name\": \"Puppet\"\n    },\n    \".pprx\": {\n        \"color\": \"#d90e09\",\n        \"name\": \"REXX\"\n    },\n    \".prawn\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".prc\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".prg\": {\n        \"color\": \"#403a40\",\n        \"name\": \"xBase\"\n    },\n    \".pro\": {\n        \"color\": \"#74283c\",\n        \"name\": \"Prolog\"\n    },\n    \".prolog\": {\n        \"color\": \"#74283c\",\n        \"name\": \"Prolog\"\n    },\n    \".prw\": {\n        \"color\": \"#403a40\",\n        \"name\": \"xBase\"\n    },\n    \".ps\": {\n        \"color\": \"#da291c\",\n        \"name\": \"PostScript\"\n    },\n    \".ps1\": {\n        \"color\": \"#012456\",\n        \"name\": \"PowerShell\"\n    },\n    \".psc\": {\n        \"color\": \"#6600cc\",\n        \"name\": \"Papyrus\"\n    },\n    \".psd1\": {\n        \"color\": \"#012456\",\n        \"name\": \"PowerShell\"\n    },\n    \".psgi\": {\n        \"color\": \"#0298c3\",\n        \"name\": \"Perl\"\n    },\n    \".psm1\": {\n        \"color\": \"#012456\",\n        \"name\": \"PowerShell\"\n    },\n    \".pug\": {\n        \"color\": \"#a86454\",\n        \"name\": \"Pug\"\n    },\n    \".purs\": {\n        \"color\": \"#1D222D\",\n        \"name\": \"PureScript\"\n    },\n    \".pwn\": {\n        \"color\": \"#dbb284\",\n        \"name\": \"Pawn\"\n    },\n    \".pxd\": {\n        \"color\": \"#fedf5b\",\n        \"name\": \"Cython\"\n    },\n    \".pxi\": {\n        \"color\": \"#fedf5b\",\n        \"name\": \"Cython\"\n    },\n    \".py\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".py3\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".pyde\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".pyi\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".pyp\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".pyt\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".pyw\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".pyx\": {\n        \"color\": \"#fedf5b\",\n        \"name\": \"Cython\"\n    },\n    \".q\": {\n        \"color\": \"#0040cd\",\n        \"name\": \"q\"\n    },\n    \".qasm\": {\n        \"color\": \"#AA70FF\",\n        \"name\": \"OpenQASM\"\n    },\n    \".qbs\": {\n        \"color\": \"#44a51c\",\n        \"name\": \"QML\"\n    },\n    \".ql\": {\n        \"color\": \"#140f46\",\n        \"name\": \"CodeQL\"\n    },\n    \".qll\": {\n        \"color\": \"#140f46\",\n        \"name\": \"CodeQL\"\n    },\n    \".qml\": {\n        \"color\": \"#44a51c\",\n        \"name\": \"QML\"\n    },\n    \".qs\": {\n        \"color\": \"#00b841\",\n        \"name\": \"Qt Script\"\n    },\n    \".r\": {\n        \"color\": \"#358a5b\",\n        \"name\": \"Rebol\"\n    },\n    \".r2\": {\n        \"color\": \"#358a5b\",\n        \"name\": \"Rebol\"\n    },\n    \".r3\": {\n        \"color\": \"#358a5b\",\n        \"name\": \"Rebol\"\n    },\n    \".rabl\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".rake\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".raku\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".rakumod\": {\n        \"color\": \"#0000fb\",\n        \"name\": \"Raku\"\n    },\n    \".raml\": {\n        \"color\": \"#77d9fb\",\n        \"name\": \"RAML\"\n    },\n    \".razor\": {\n        \"color\": \"#512be4\",\n        \"name\": \"HTML+Razor\"\n    },\n    \".rb\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".rbi\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".rbuild\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".rbw\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".rbx\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".rbxs\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".rd\": {\n        \"color\": \"#198CE7\",\n        \"name\": \"R\"\n    },\n    \".re\": {\n        \"color\": \"#ff5847\",\n        \"name\": \"Reason\"\n    },\n    \".reb\": {\n        \"color\": \"#358a5b\",\n        \"name\": \"Rebol\"\n    },\n    \".rebol\": {\n        \"color\": \"#358a5b\",\n        \"name\": \"Rebol\"\n    },\n    \".red\": {\n        \"color\": \"#f50000\",\n        \"name\": \"Red\"\n    },\n    \".reds\": {\n        \"color\": \"#f50000\",\n        \"name\": \"Red\"\n    },\n    \".rego\": {\n        \"color\": \"#7d9199\",\n        \"name\": \"Open Policy Agent\"\n    },\n    \".rei\": {\n        \"color\": \"#ff5847\",\n        \"name\": \"Reason\"\n    },\n    \".res\": {\n        \"color\": \"#ed5051\",\n        \"name\": \"ReScript\"\n    },\n    \".rex\": {\n        \"color\": \"#d90e09\",\n        \"name\": \"REXX\"\n    },\n    \".rexx\": {\n        \"color\": \"#d90e09\",\n        \"name\": \"REXX\"\n    },\n    \".rg\": {\n        \"color\": \"#cc0088\",\n        \"name\": \"Rouge\"\n    },\n    \".rhtml\": {\n        \"color\": \"#701516\",\n        \"name\": \"HTML+ERB\"\n    },\n    \".ring\": {\n        \"color\": \"#2D54CB\",\n        \"name\": \"Ring\"\n    },\n    \".riot\": {\n        \"color\": \"#A71E49\",\n        \"name\": \"Riot\"\n    },\n    \".rkt\": {\n        \"color\": \"#3c5caa\",\n        \"name\": \"Racket\"\n    },\n    \".rktd\": {\n        \"color\": \"#3c5caa\",\n        \"name\": \"Racket\"\n    },\n    \".rktl\": {\n        \"color\": \"#3c5caa\",\n        \"name\": \"Racket\"\n    },\n    \".rl\": {\n        \"color\": \"#9d5200\",\n        \"name\": \"Ragel\"\n    },\n    \".rnh\": {\n        \"color\": \"#665a4e\",\n        \"name\": \"RUNOFF\"\n    },\n    \".rno\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".robot\": {\n        \"color\": \"#00c0b5\",\n        \"name\": \"RobotFramework\"\n    },\n    \".rockspec\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".roff\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".rpy\": {\n        \"color\": \"#ff7f7f\",\n        \"name\": \"Ren'Py\"\n    },\n    \".rs\": {\n        \"color\": \"#dea584\",\n        \"name\": \"Rust\"\n    },\n    \".rs.in\": {\n        \"color\": \"#dea584\",\n        \"name\": \"Rust\"\n    },\n    \".rsc\": {\n        \"color\": \"#fffaa0\",\n        \"name\": \"Rascal\"\n    },\n    \".rsx\": {\n        \"color\": \"#198CE7\",\n        \"name\": \"R\"\n    },\n    \".ru\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".ruby\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".s\": {\n        \"color\": \"#005daa\",\n        \"name\": \"Motorola 68K Assembly\"\n    },\n    \".sas\": {\n        \"color\": \"#B34936\",\n        \"name\": \"SAS\"\n    },\n    \".sass\": {\n        \"color\": \"#a53b70\",\n        \"name\": \"Sass\"\n    },\n    \".sats\": {\n        \"color\": \"#1ac620\",\n        \"name\": \"ATS\"\n    },\n    \".sbt\": {\n        \"color\": \"#c22d40\",\n        \"name\": \"Scala\"\n    },\n    \".sc\": {\n        \"color\": \"#46390b\",\n        \"name\": \"SuperCollider\"\n    },\n    \".scad\": {\n        \"color\": \"#e5cd45\",\n        \"name\": \"OpenSCAD\"\n    },\n    \".scala\": {\n        \"color\": \"#c22d40\",\n        \"name\": \"Scala\"\n    },\n    \".scaml\": {\n        \"color\": \"#bd181a\",\n        \"name\": \"Scaml\"\n    },\n    \".scd\": {\n        \"color\": \"#46390b\",\n        \"name\": \"SuperCollider\"\n    },\n    \".sce\": {\n        \"color\": \"#ca0f21\",\n        \"name\": \"Scilab\"\n    },\n    \".sch\": {\n        \"color\": \"#1e4aec\",\n        \"name\": \"Scheme\"\n    },\n    \".sci\": {\n        \"color\": \"#ca0f21\",\n        \"name\": \"Scilab\"\n    },\n    \".scm\": {\n        \"color\": \"#1e4aec\",\n        \"name\": \"Scheme\"\n    },\n    \".sco\": {\n        \"color\": \"#1a1a1a\",\n        \"name\": \"Csound Score\"\n    },\n    \".scpt\": {\n        \"color\": \"#101F1F\",\n        \"name\": \"AppleScript\"\n    },\n    \".scrbl\": {\n        \"color\": \"#3c5caa\",\n        \"name\": \"Racket\"\n    },\n    \".scss\": {\n        \"color\": \"#c6538c\",\n        \"name\": \"SCSS\"\n    },\n    \".sed\": {\n        \"color\": \"#64b970\",\n        \"name\": \"sed\"\n    },\n    \".self\": {\n        \"color\": \"#0579aa\",\n        \"name\": \"Self\"\n    },\n    \".sexp\": {\n        \"color\": \"#3fb68b\",\n        \"name\": \"Common Lisp\"\n    },\n    \".sh\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".sh.in\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".shader\": {\n        \"color\": \"#222c37\",\n        \"name\": \"ShaderLab\"\n    },\n    \".shen\": {\n        \"color\": \"#120F14\",\n        \"name\": \"Shen\"\n    },\n    \".sig\": {\n        \"color\": \"#dc566d\",\n        \"name\": \"Standard ML\"\n    },\n    \".sj\": {\n        \"color\": \"#ff0c5a\",\n        \"name\": \"Objective-J\"\n    },\n    \".sjs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".sl\": {\n        \"color\": \"#007eff\",\n        \"name\": \"Slash\"\n    },\n    \".sld\": {\n        \"color\": \"#1e4aec\",\n        \"name\": \"Scheme\"\n    },\n    \".slim\": {\n        \"color\": \"#2b2b2b\",\n        \"name\": \"Slim\"\n    },\n    \".sls\": {\n        \"color\": \"#1e4aec\",\n        \"name\": \"Scheme\"\n    },\n    \".sma\": {\n        \"color\": \"#dbb284\",\n        \"name\": \"Pawn\"\n    },\n    \".smk\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".sml\": {\n        \"color\": \"#dc566d\",\n        \"name\": \"Standard ML\"\n    },\n    \".snip\": {\n        \"color\": \"#199f4b\",\n        \"name\": \"Vim Snippet\"\n    },\n    \".snippet\": {\n        \"color\": \"#199f4b\",\n        \"name\": \"Vim Snippet\"\n    },\n    \".snippets\": {\n        \"color\": \"#199f4b\",\n        \"name\": \"Vim Snippet\"\n    },\n    \".sol\": {\n        \"color\": \"#AA6746\",\n        \"name\": \"Solidity\"\n    },\n    \".soy\": {\n        \"color\": \"#0d948f\",\n        \"name\": \"Closure Templates\"\n    },\n    \".sp\": {\n        \"color\": \"#f69e1d\",\n        \"name\": \"SourcePawn\"\n    },\n    \".spc\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".spec\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".spin\": {\n        \"color\": \"#7fa2a7\",\n        \"name\": \"Propeller Spin\"\n    },\n    \".sps\": {\n        \"color\": \"#1e4aec\",\n        \"name\": \"Scheme\"\n    },\n    \".sqf\": {\n        \"color\": \"#3F3F3F\",\n        \"name\": \"SQF\"\n    },\n    \".sql\": {\n        \"color\": \"#e38c00\",\n        \"name\": \"TSQL\"\n    },\n    \".sra\": {\n        \"color\": \"#8f0f8d\",\n        \"name\": \"PowerBuilder\"\n    },\n    \".srt\": {\n        \"color\": \"#348a34\",\n        \"name\": \"SRecode Template\"\n    },\n    \".sru\": {\n        \"color\": \"#8f0f8d\",\n        \"name\": \"PowerBuilder\"\n    },\n    \".srw\": {\n        \"color\": \"#8f0f8d\",\n        \"name\": \"PowerBuilder\"\n    },\n    \".ss\": {\n        \"color\": \"#1e4aec\",\n        \"name\": \"Scheme\"\n    },\n    \".ssjs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".sss\": {\n        \"color\": \"#2fcc9f\",\n        \"name\": \"SugarSS\"\n    },\n    \".st\": {\n        \"color\": \"#3fb34f\",\n        \"name\": \"StringTemplate\"\n    },\n    \".stan\": {\n        \"color\": \"#b2011d\",\n        \"name\": \"Stan\"\n    },\n    \".sthlp\": {\n        \"color\": \"#1a5f91\",\n        \"name\": \"Stata\"\n    },\n    \".story\": {\n        \"color\": \"#5B2063\",\n        \"name\": \"Gherkin\"\n    },\n    \".sty\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".styl\": {\n        \"color\": \"#ff6347\",\n        \"name\": \"Stylus\"\n    },\n    \".sv\": {\n        \"color\": \"#DAE1C2\",\n        \"name\": \"SystemVerilog\"\n    },\n    \".svelte\": {\n        \"color\": \"#ff3e00\",\n        \"name\": \"Svelte\"\n    },\n    \".svh\": {\n        \"color\": \"#DAE1C2\",\n        \"name\": \"SystemVerilog\"\n    },\n    \".swift\": {\n        \"color\": \"#F05138\",\n        \"name\": \"Swift\"\n    },\n    \".t\": {\n        \"color\": \"#cf142b\",\n        \"name\": \"Turing\"\n    },\n    \".tac\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".tcc\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".tcl\": {\n        \"color\": \"#e4cc98\",\n        \"name\": \"Tcl\"\n    },\n    \".tcl.in\": {\n        \"color\": \"#e4cc98\",\n        \"name\": \"Tcl\"\n    },\n    \".tesc\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".tese\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".tex\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".thor\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".thrift\": {\n        \"color\": \"#D12127\",\n        \"name\": \"Thrift\"\n    },\n    \".thy\": {\n        \"color\": \"#FEFE00\",\n        \"name\": \"Isabelle\"\n    },\n    \".tla\": {\n        \"color\": \"#4b0079\",\n        \"name\": \"TLA\"\n    },\n    \".tm\": {\n        \"color\": \"#e4cc98\",\n        \"name\": \"Tcl\"\n    },\n    \".tmac\": {\n        \"color\": \"#ecdebe\",\n        \"name\": \"Roff\"\n    },\n    \".tmux\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".toc\": {\n        \"color\": \"#3D6117\",\n        \"name\": \"TeX\"\n    },\n    \".tool\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    },\n    \".tpb\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".tpl\": {\n        \"color\": \"#f0c040\",\n        \"name\": \"Smarty\"\n    },\n    \".tpp\": {\n        \"color\": \"#f34b7d\",\n        \"name\": \"C++\"\n    },\n    \".tps\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".trg\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".ts\": {\n        \"color\": \"#2b7489\",\n        \"name\": \"TypeScript\"\n    },\n    \".tst\": {\n        \"color\": \"#ca0f21\",\n        \"name\": \"Scilab\"\n    },\n    \".tsx\": {\n        \"color\": \"#2b7489\",\n        \"name\": \"TypeScript\"\n    },\n    \".tu\": {\n        \"color\": \"#cf142b\",\n        \"name\": \"Turing\"\n    },\n    \".twig\": {\n        \"color\": \"#c1d026\",\n        \"name\": \"Twig\"\n    },\n    \".txl\": {\n        \"color\": \"#0178b8\",\n        \"name\": \"TXL\"\n    },\n    \".uc\": {\n        \"color\": \"#a54c4d\",\n        \"name\": \"UnrealScript\"\n    },\n    \".udo\": {\n        \"color\": \"#1a1a1a\",\n        \"name\": \"Csound\"\n    },\n    \".uno\": {\n        \"color\": \"#9933cc\",\n        \"name\": \"Uno\"\n    },\n    \".upc\": {\n        \"color\": \"#4e3617\",\n        \"name\": \"Unified Parallel C\"\n    },\n    \".ur\": {\n        \"color\": \"#ccccee\",\n        \"name\": \"UrWeb\"\n    },\n    \".urs\": {\n        \"color\": \"#ccccee\",\n        \"name\": \"UrWeb\"\n    },\n    \".v\": {\n        \"color\": \"#b2b7f8\",\n        \"name\": \"Verilog\"\n    },\n    \".vala\": {\n        \"color\": \"#fbe5cd\",\n        \"name\": \"Vala\"\n    },\n    \".vapi\": {\n        \"color\": \"#fbe5cd\",\n        \"name\": \"Vala\"\n    },\n    \".vark\": {\n        \"color\": \"#82937f\",\n        \"name\": \"Gosu\"\n    },\n    \".vb\": {\n        \"color\": \"#945db7\",\n        \"name\": \"Visual Basic .NET\"\n    },\n    \".vba\": {\n        \"color\": \"#199f4b\",\n        \"name\": \"Vim Script\"\n    },\n    \".vbhtml\": {\n        \"color\": \"#945db7\",\n        \"name\": \"Visual Basic .NET\"\n    },\n    \".vbs\": {\n        \"color\": \"#15dcdc\",\n        \"name\": \"VBScript\"\n    },\n    \".vcl\": {\n        \"color\": \"#148AA8\",\n        \"name\": \"VCL\"\n    },\n    \".veo\": {\n        \"color\": \"#b2b7f8\",\n        \"name\": \"Verilog\"\n    },\n    \".vert\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".vh\": {\n        \"color\": \"#DAE1C2\",\n        \"name\": \"SystemVerilog\"\n    },\n    \".vhd\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vhdl\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vhf\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vhi\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vho\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vhs\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vht\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".vhw\": {\n        \"color\": \"#adb2cb\",\n        \"name\": \"VHDL\"\n    },\n    \".view.lkml\": {\n        \"color\": \"#652B81\",\n        \"name\": \"LookML\"\n    },\n    \".vim\": {\n        \"color\": \"#199f4b\",\n        \"name\": \"Vim Script\"\n    },\n    \".vmb\": {\n        \"color\": \"#199f4b\",\n        \"name\": \"Vim Script\"\n    },\n    \".volt\": {\n        \"color\": \"#1F1F1F\",\n        \"name\": \"Volt\"\n    },\n    \".vrx\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".vsh\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".vshader\": {\n        \"color\": \"#5686a5\",\n        \"name\": \"GLSL\"\n    },\n    \".vue\": {\n        \"color\": \"#41b883\",\n        \"name\": \"Vue\"\n    },\n    \".vw\": {\n        \"color\": \"#dad8d8\",\n        \"name\": \"PLSQL\"\n    },\n    \".w\": {\n        \"color\": \"#5ce600\",\n        \"name\": \"OpenEdge ABL\"\n    },\n    \".wast\": {\n        \"color\": \"#04133b\",\n        \"name\": \"WebAssembly\"\n    },\n    \".wat\": {\n        \"color\": \"#04133b\",\n        \"name\": \"WebAssembly\"\n    },\n    \".watchr\": {\n        \"color\": \"#701516\",\n        \"name\": \"Ruby\"\n    },\n    \".wdl\": {\n        \"color\": \"#42f1f4\",\n        \"name\": \"wdl\"\n    },\n    \".wisp\": {\n        \"color\": \"#7582D1\",\n        \"name\": \"wisp\"\n    },\n    \".wl\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".wlk\": {\n        \"color\": \"#a23738\",\n        \"name\": \"Wollok\"\n    },\n    \".wlt\": {\n        \"color\": \"#dd1100\",\n        \"name\": \"Mathematica\"\n    },\n    \".wlua\": {\n        \"color\": \"#000080\",\n        \"name\": \"Lua\"\n    },\n    \".wsgi\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".x10\": {\n        \"color\": \"#4B6BEF\",\n        \"name\": \"X10\"\n    },\n    \".x68\": {\n        \"color\": \"#005daa\",\n        \"name\": \"Motorola 68K Assembly\"\n    },\n    \".xc\": {\n        \"color\": \"#99DA07\",\n        \"name\": \"XC\"\n    },\n    \".xht\": {\n        \"color\": \"#e34c26\",\n        \"name\": \"HTML\"\n    },\n    \".xhtml\": {\n        \"color\": \"#e34c26\",\n        \"name\": \"HTML\"\n    },\n    \".xojo_code\": {\n        \"color\": \"#81bd41\",\n        \"name\": \"Xojo\"\n    },\n    \".xojo_menu\": {\n        \"color\": \"#81bd41\",\n        \"name\": \"Xojo\"\n    },\n    \".xojo_report\": {\n        \"color\": \"#81bd41\",\n        \"name\": \"Xojo\"\n    },\n    \".xojo_script\": {\n        \"color\": \"#81bd41\",\n        \"name\": \"Xojo\"\n    },\n    \".xojo_toolbar\": {\n        \"color\": \"#81bd41\",\n        \"name\": \"Xojo\"\n    },\n    \".xojo_window\": {\n        \"color\": \"#81bd41\",\n        \"name\": \"Xojo\"\n    },\n    \".xpy\": {\n        \"color\": \"#3572A5\",\n        \"name\": \"Python\"\n    },\n    \".xq\": {\n        \"color\": \"#5232e7\",\n        \"name\": \"XQuery\"\n    },\n    \".xql\": {\n        \"color\": \"#5232e7\",\n        \"name\": \"XQuery\"\n    },\n    \".xqm\": {\n        \"color\": \"#5232e7\",\n        \"name\": \"XQuery\"\n    },\n    \".xquery\": {\n        \"color\": \"#5232e7\",\n        \"name\": \"XQuery\"\n    },\n    \".xqy\": {\n        \"color\": \"#5232e7\",\n        \"name\": \"XQuery\"\n    },\n    \".xrl\": {\n        \"color\": \"#B83998\",\n        \"name\": \"Erlang\"\n    },\n    \".xsh\": {\n        \"color\": \"#285EEF\",\n        \"name\": \"Xonsh\"\n    },\n    \".xsjs\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".xsjslib\": {\n        \"color\": \"#f1e05a\",\n        \"name\": \"JavaScript\"\n    },\n    \".xsl\": {\n        \"color\": \"#EB8CEB\",\n        \"name\": \"XSLT\"\n    },\n    \".xslt\": {\n        \"color\": \"#EB8CEB\",\n        \"name\": \"XSLT\"\n    },\n    \".xtend\": {\n        \"color\": \"#24255d\",\n        \"name\": \"Xtend\"\n    },\n    \".xzap\": {\n        \"color\": \"#0d665e\",\n        \"name\": \"ZAP\"\n    },\n    \".y\": {\n        \"color\": \"#4B6C4B\",\n        \"name\": \"Yacc\"\n    },\n    \".yacc\": {\n        \"color\": \"#4B6C4B\",\n        \"name\": \"Yacc\"\n    },\n    \".yap\": {\n        \"color\": \"#74283c\",\n        \"name\": \"Prolog\"\n    },\n    \".yar\": {\n        \"color\": \"#220000\",\n        \"name\": \"YARA\"\n    },\n    \".yara\": {\n        \"color\": \"#220000\",\n        \"name\": \"YARA\"\n    },\n    \".yasnippet\": {\n        \"color\": \"#32AB90\",\n        \"name\": \"YASnippet\"\n    },\n    \".yrl\": {\n        \"color\": \"#B83998\",\n        \"name\": \"Erlang\"\n    },\n    \".yy\": {\n        \"color\": \"#4B6C4B\",\n        \"name\": \"Yacc\"\n    },\n    \".zap\": {\n        \"color\": \"#0d665e\",\n        \"name\": \"ZAP\"\n    },\n    \".zep\": {\n        \"color\": \"#118f9e\",\n        \"name\": \"Zephir\"\n    },\n    \".zig\": {\n        \"color\": \"#ec915c\",\n        \"name\": \"Zig\"\n    },\n    \".zil\": {\n        \"color\": \"#dc75e5\",\n        \"name\": \"ZIL\"\n    },\n    \".zimpl\": {\n        \"color\": \"#d67711\",\n        \"name\": \"Zimpl\"\n    },\n    \".zmpl\": {\n        \"color\": \"#d67711\",\n        \"name\": \"Zimpl\"\n    },\n    \".zpl\": {\n        \"color\": \"#d67711\",\n        \"name\": \"Zimpl\"\n    },\n    \".zs\": {\n        \"color\": \"#00BCD1\",\n        \"name\": \"ZenScript\"\n    },\n    \".zsh\": {\n        \"color\": \"#89e051\",\n        \"name\": \"Shell\"\n    }\n}"
  },
  {
    "path": "backend/src/data/github/graphql/__init__.py",
    "content": "from src.data.github.graphql.commit import get_commits\nfrom src.data.github.graphql.models import RawCommit, RawRepo\nfrom src.data.github.graphql.repo import get_repo\nfrom src.data.github.graphql.template import (\n    GraphQLErrorMissingNode,\n    GraphQLErrorRateLimit,\n    GraphQLErrorTimeout,\n    get_query_limit,\n)\nfrom src.data.github.graphql.user.contribs.contribs import (\n    get_user_contribution_calendar,\n    get_user_contribution_events,\n)\nfrom src.data.github.graphql.user.contribs.models import (\n    RawCalendar,\n    RawEvents,\n    RawEventsCommit,\n    RawEventsEvent,\n)\nfrom src.data.github.graphql.user.follows.follows import (\n    get_user_followers,\n    get_user_following,\n)\nfrom src.data.github.graphql.user.follows.models import RawFollows\n\n__all__ = [\n    \"get_commits\",\n    \"RawCommit\",\n    \"RawRepo\",\n    \"get_repo\",\n    \"GraphQLErrorMissingNode\",\n    \"GraphQLErrorRateLimit\",\n    \"GraphQLErrorTimeout\",\n    \"get_query_limit\",\n    \"get_user_contribution_calendar\",\n    \"get_user_contribution_events\",\n    \"RawCalendar\",\n    \"RawEvents\",\n    \"RawEventsCommit\",\n    \"RawEventsEvent\",\n    \"get_user_followers\",\n    \"get_user_following\",\n    \"RawFollows\",\n]\n"
  },
  {
    "path": "backend/src/data/github/graphql/commit.py",
    "content": "from typing import List, Optional\n\nfrom src.constants import PR_FILES\nfrom src.data.github.graphql.models import RawCommit\nfrom src.data.github.graphql.template import (\n    GraphQLError,\n    GraphQLErrorMissingNode,\n    GraphQLErrorRateLimit,\n    GraphQLErrorTimeout,\n    get_template,\n)\n\n\ndef get_commits(\n    node_ids: List[str], access_token: Optional[str] = None, catch_errors: bool = False\n) -> List[Optional[RawCommit]]:\n    \"\"\"\n    Gets all repository data from graphql\n    :param access_token: GitHub access token\n    :param node_ids: List of node ids\n    :return: List of commits\n    \"\"\"\n\n    if PR_FILES == 0:  # type: ignore\n        query = {\n            \"variables\": {\"ids\": node_ids},\n            \"query\": \"\"\"\n            query getCommits($ids: [ID!]!) {\n                nodes(ids: $ids) {\n                    ... on Commit {\n                        additions\n                        deletions\n                        changedFiles\n                        url\n                    }\n                }\n            }\n            \"\"\",\n        }\n    else:\n        query = {\n            \"variables\": {\"ids\": node_ids, \"first\": PR_FILES},\n            \"query\": \"\"\"\n            query getCommits($ids: [ID!]!, $first: Int!) {\n                nodes(ids: $ids) {\n                    ... on Commit {\n                        additions\n                        deletions\n                        changedFiles\n                        url\n                        associatedPullRequests(first: 1) {\n                            nodes {\n                                changedFiles\n                                additions\n                                deletions\n                                files(first: $first) {\n                                    nodes {\n                                        path\n                                        additions\n                                        deletions\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            \"\"\",\n        }\n\n    try:\n        raw_commits = get_template(query, access_token)[\"data\"][\"nodes\"]\n    except GraphQLErrorMissingNode as e:\n        return (\n            get_commits(node_ids[: e.node], access_token)\n            + [None]\n            + get_commits(node_ids[e.node + 1 :], access_token)\n        )\n    except (GraphQLErrorRateLimit, GraphQLErrorTimeout, GraphQLError) as e:\n        if catch_errors:\n            return [None for _ in node_ids]\n        raise e\n\n    out: List[Optional[RawCommit]] = []\n    for raw_commit in raw_commits:\n        try:\n            if \"associatedPullRequests\" not in raw_commit:\n                raw_commit[\"associatedPullRequests\"] = {\"nodes\": []}\n            out.append(RawCommit.model_validate(raw_commit))\n        except Exception as e:\n            if catch_errors:\n                out.append(None)\n            else:\n                raise e\n    return out\n"
  },
  {
    "path": "backend/src/data/github/graphql/models.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel, Field\n\n\nclass RawCommitPRFileNode(BaseModel):\n    path: str\n    additions: int\n    deletions: int\n\n\nclass RawCommitPRFile(BaseModel):\n    nodes: List[RawCommitPRFileNode]\n\n\nclass RawCommitPRNode(BaseModel):\n    changed_files: int = Field(alias=\"changedFiles\")\n    additions: int\n    deletions: int\n    files: RawCommitPRFile\n\n\nclass RawCommitPR(BaseModel):\n    nodes: List[RawCommitPRNode]\n\n\nclass RawCommit(BaseModel):\n    additions: int\n    deletions: int\n    changed_files: int = Field(alias=\"changedFiles\")\n    url: str\n    prs: RawCommitPR = Field(alias=\"associatedPullRequests\")\n\n\nclass RawRepoLanguageNode(BaseModel):\n    name: str\n    color: Optional[str]\n\n\nclass RawRepoLanguageEdge(BaseModel):\n    node: RawRepoLanguageNode\n    size: int\n\n\nclass RawRepoLanguage(BaseModel):\n    total_count: int = Field(alias=\"totalCount\")\n    total_size: int = Field(alias=\"totalSize\")\n    edges: List[RawRepoLanguageEdge]\n\n\nclass RawRepo(BaseModel):\n    is_private: bool = Field(alias=\"isPrivate\")\n    fork_count: int = Field(alias=\"forkCount\")\n    stargazer_count: int = Field(alias=\"stargazerCount\")\n    languages: RawRepoLanguage\n"
  },
  {
    "path": "backend/src/data/github/graphql/repo.py",
    "content": "from typing import Optional\n\nfrom src.data.github.graphql.models import RawRepo\nfrom src.data.github.graphql.template import get_template\n\n\ndef get_repo(\n    owner: str,\n    repo: str,\n    access_token: Optional[str] = None,\n    catch_errors: bool = False,\n) -> Optional[RawRepo]:\n    \"\"\"\n    Gets all repository data from graphql\n    :param access_token: GitHub access token\n    :param owner: GitHub owner\n    :param repo: GitHub repository\n    :return: RawRepo object or None if repo not present\n    \"\"\"\n    query = {\n        \"variables\": {\"owner\": owner, \"repo\": repo},\n        \"query\": \"\"\"\n        query getRepo($owner: String!, $repo: String!) {\n            repository(owner: $owner, name: $repo) {\n                isPrivate,\n                forkCount,\n                stargazerCount,\n                languages(first: 10){\n                    totalCount,\n                    totalSize,\n                    edges{\n                        node {\n                            name,\n                            color,\n                        },\n                        size,\n                    },\n                },\n            }\n        }\n        \"\"\",\n    }\n\n    try:\n        raw_repo = get_template(query, access_token)[\"data\"][\"repository\"]\n        return RawRepo.model_validate(raw_repo)\n    except Exception as e:\n        if catch_errors:\n            return None\n        raise e\n"
  },
  {
    "path": "backend/src/data/github/graphql/template.py",
    "content": "import logging\nfrom datetime import datetime\nfrom typing import Any, Dict, Optional, Tuple\n\nimport requests\nfrom requests.exceptions import ReadTimeout\n\nfrom src.constants import TIMEOUT\nfrom src.data.github.utils import get_access_token\n\ns = requests.session()\n\n\nclass GraphQLError(Exception):\n    pass\n\n\nclass GraphQLErrorMissingNode(Exception):\n    def __init__(self, node: int, *args: Tuple[Any], **kwargs: Dict[str, Any]):\n        super(Exception, self).__init__(*args, **kwargs)\n        self.node = node\n\n\nclass GraphQLErrorRateLimit(Exception):\n    pass\n\n\nclass GraphQLErrorTimeout(Exception):\n    pass\n\n\ndef get_template(\n    query: Dict[str, Any], access_token: Optional[str] = None, retries: int = 0\n) -> Dict[str, Any]:\n    \"\"\"\n    Template for interacting with the GitHub GraphQL API\n    :param query: The query to be sent to the GitHub GraphQL API\n    :param access_token: The access token to be used for the query\n    :param retries: The number of retries to be made for Auth Exceptions\n    :return: The response from the GitHub GraphQL API\n    \"\"\"\n    start = datetime.now()\n    new_access_token = get_access_token(access_token)\n    headers: Dict[str, str] = {\"Authorization\": f\"bearer {new_access_token}\"}\n\n    try:\n        r = s.post(\n            \"https://api.github.com/graphql\",\n            json=query,\n            headers=headers,\n            timeout=TIMEOUT,\n        )\n    except ReadTimeout:\n        raise GraphQLErrorTimeout(\"GraphQL Error: Request Timeout\")\n\n    print(\"GraphQL\", new_access_token, datetime.now() - start)\n    if r.status_code == 200:\n        data = r.json()\n        if \"errors\" in data:\n            if (\n                \"type\" in data[\"errors\"][0]\n                and data[\"errors\"][0][\"type\"] in [\"SERVICE_UNAVAILABLE\", \"NOT_FOUND\"]\n                and \"path\" in data[\"errors\"][0]\n                and isinstance(data[\"errors\"][0][\"path\"], list)\n                and data[\"errors\"][0][\"path\"][0] == \"nodes\"\n            ):\n                raise GraphQLErrorMissingNode(node=int(data[\"errors\"][0][\"path\"][1]))\n\n            if retries < 2:\n                print(\"GraphQL Error, Retrying:\", new_access_token)\n                return get_template(query, access_token, retries + 1)\n\n            raise GraphQLError(\"GraphQL Error: \" + str(data[\"errors\"]))\n\n        return data\n\n    if r.status_code in [401, 403]:\n        if retries < 2:\n            print(\"GraphQL Error, Retrying:\", new_access_token)\n            return get_template(query, access_token, retries + 1)\n        raise GraphQLErrorRateLimit(\"GraphQL Error: Unauthorized\")\n\n    if r.status_code == 502:\n        raise GraphQLErrorTimeout(\"GraphQL Error: Request Timeout\")\n\n    raise GraphQLError(f\"GraphQL Error: {str(r.status_code)}\")\n\n\ndef get_query_limit(access_token: str) -> int:\n    \"\"\"\n    Get the current rate limit for the GitHub GraphQL API\n    :param access_token: The access token to be used for the query\n    :return: The current rate limit for the GitHub GraphQL API\n    \"\"\"\n    try:\n        data = get_template(\n            {\"query\": \"query { rateLimit { remaining } }\"}, access_token\n        )\n        return data[\"data\"][\"rateLimit\"][\"remaining\"]\n    except Exception as e:\n        logging.exception(e)\n        return -1\n"
  },
  {
    "path": "backend/src/data/github/graphql/user/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/data/github/graphql/user/contribs/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/data/github/graphql/user/contribs/contribs.py",
    "content": "# import json\nfrom datetime import datetime\nfrom typing import Optional\n\nfrom src.data.github.graphql.template import get_template\nfrom src.data.github.graphql.user.contribs.models import RawCalendar, RawEvents\n\n\ndef get_user_contribution_calendar(\n    user_id: str,\n    start_date: datetime,\n    end_date: datetime,\n    access_token: Optional[str] = None,\n) -> RawCalendar:\n    \"\"\"Gets contribution calendar for a given time period (max one year)\"\"\"\n    if (end_date - start_date).days > 365:\n        raise ValueError(\"date range can be at most 1 year\")\n    query = {\n        \"variables\": {\n            \"login\": user_id,\n            \"startDate\": start_date.strftime(\"%Y-%m-%dT%H:%M:%SZ\"),\n            \"endDate\": end_date.strftime(\"%Y-%m-%dT%H:%M:%SZ\"),\n        },\n        \"query\": \"\"\"\n        query getUser($login: String!, $startDate: DateTime!, $endDate: DateTime!){\n            user(login: $login){\n                contributionsCollection(from: $startDate, to: $endDate){\n                    contributionCalendar{\n                        weeks{\n                            contributionDays{\n                                date\n                                weekday\n                                contributionCount\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        \"\"\",\n    }\n\n    raw_data = get_template(query, access_token)\n    output = raw_data[\"data\"][\"user\"][\"contributionsCollection\"][\"contributionCalendar\"]\n    return RawCalendar.model_validate(output)\n\n\ndef get_user_contribution_events(\n    user_id: str,\n    start_date: datetime,\n    end_date: datetime,\n    max_repos: int = 100,\n    first: int = 100,\n    after: str = \"\",\n    access_token: Optional[str] = None,\n) -> RawEvents:\n    \"\"\"Fetches user contributions (commits, issues, prs, reviews)\"\"\"\n    query = {\n        \"variables\": {\n            \"login\": user_id,\n            \"startDate\": start_date.strftime(\"%Y-%m-%dT%H:%M:%SZ\"),\n            \"endDate\": end_date.strftime(\"%Y-%m-%dT%H:%M:%SZ\"),\n            \"maxRepos\": max_repos,\n            \"first\": first,\n            \"after\": after,\n        },\n        \"query\": \"\"\"\n        query getUser($login: String!, $startDate: DateTime!, $endDate: DateTime!, $maxRepos: Int!, $first: Int!, $after: String!) {\n            user(login: $login){\n                contributionsCollection(from: $startDate, to: $endDate){\n                    commitContributionsByRepository(maxRepositories: $maxRepos){\n                        repository{\n                            nameWithOwner,\n                        },\n                        totalCount:contributions(first: 1){\n                            totalCount\n                        }\n                        contributions(first: $first, after: $after){\n                            nodes{\n                                commitCount,\n                                occurredAt,\n                            }\n                            pageInfo{\n                                hasNextPage,\n                                endCursor\n                            }\n                        }\n                    }\n                    issueContributionsByRepository(maxRepositories: $maxRepos){\n                        repository{\n                            nameWithOwner\n                        },\n                        totalCount:contributions(first: 1){\n                            totalCount\n                        }\n                        contributions(first: $first, after: $after){\n                            nodes{\n                                occurredAt,\n                            }\n                            pageInfo{\n                                hasNextPage,\n                                endCursor\n                            }\n                        }\n                    }\n                    pullRequestContributionsByRepository(maxRepositories: $maxRepos){\n                        repository{\n                            nameWithOwner\n                        },\n                        totalCount:contributions(first: 1){\n                            totalCount\n                        }\n                        contributions(first: $first, after: $after){\n                            nodes{\n                                occurredAt,\n                            }\n                            pageInfo{\n                                hasNextPage,\n                                endCursor\n                            }\n                        }\n                    }\n                    pullRequestReviewContributionsByRepository(maxRepositories: $maxRepos){\n                        repository{\n                            nameWithOwner\n                        },\n                        totalCount:contributions(first: 1){\n                            totalCount\n                        }\n                        contributions(first: $first, after: $after){\n                            nodes{\n                                occurredAt,\n                            }\n                            pageInfo{\n                                hasNextPage,\n                                endCursor\n                            }\n                        }\n                    },\n                    repositoryContributions(first: $maxRepos){\n                        totalCount\n                        nodes{\n                            repository{\n                                nameWithOwner,\n                            }\n                            occurredAt,\n                        }\n                    },\n                },\n            }\n        }\n        \"\"\",\n    }\n\n    raw_data = get_template(query, access_token)\n    output = raw_data[\"data\"][\"user\"][\"contributionsCollection\"]\n    return RawEvents.model_validate(output)\n"
  },
  {
    "path": "backend/src/data/github/graphql/user/contribs/models.py",
    "content": "from datetime import date, datetime\nfrom typing import List, Optional\n\nfrom pydantic import BaseModel, Field\n\n\nclass RawCalendarDay(BaseModel):\n    date: date\n    weekday: int\n    count: int = Field(alias=\"contributionCount\")\n\n\nclass RawCalendarWeek(BaseModel):\n    contribution_days: List[RawCalendarDay] = Field(alias=\"contributionDays\")\n\n\nclass RawCalendar(BaseModel):\n    weeks: List[RawCalendarWeek]\n\n\nclass RawEventsRepoName(BaseModel):\n    name: str = Field(alias=\"nameWithOwner\")\n\n\nclass RawEventsCount(BaseModel):\n    count: int = Field(alias=\"totalCount\")\n\n\nclass RawEventsCommit(BaseModel):\n    count: int = Field(alias=\"commitCount\")\n    occurred_at: datetime = Field(alias=\"occurredAt\")\n\n\nclass RawEventsEvent(BaseModel):\n    occurred_at: datetime = Field(alias=\"occurredAt\")\n\n\nclass RawEventsPageInfo(BaseModel):\n    has_next_page: bool = Field(alias=\"hasNextPage\")\n    end_cursor: Optional[str] = Field(alias=\"endCursor\")\n\n    class Config:\n        allow_none = True\n\n\nclass RawEventsCommits(BaseModel):\n    nodes: List[RawEventsCommit]\n    page_info: RawEventsPageInfo = Field(alias=\"pageInfo\")\n\n\nclass RawEventsContribs(BaseModel):\n    nodes: List[RawEventsEvent]\n    page_info: RawEventsPageInfo = Field(alias=\"pageInfo\")\n\n\nclass RawEventsRepoCommits(BaseModel):\n    repo: RawEventsRepoName = Field(alias=\"repository\")\n    count: RawEventsCount = Field(alias=\"totalCount\")\n    contribs: RawEventsCommits = Field(alias=\"contributions\")\n\n\nclass RawEventsRepo(BaseModel):\n    repo: RawEventsRepoName = Field(alias=\"repository\")\n    count: RawEventsCount = Field(alias=\"totalCount\")\n    contribs: RawEventsContribs = Field(alias=\"contributions\")\n\n\nclass RawEventsRepoEvent(BaseModel):\n    repo: RawEventsRepoName = Field(alias=\"repository\")\n    occurred_at: datetime = Field(alias=\"occurredAt\")\n\n\nclass RawEventsRepoContribs(BaseModel):\n    count: int = Field(alias=\"totalCount\")\n    nodes: List[RawEventsRepoEvent]\n\n\nclass RawEvents(BaseModel):\n    commit_contribs_by_repo: List[RawEventsRepoCommits] = Field(\n        alias=\"commitContributionsByRepository\"\n    )\n    issue_contribs_by_repo: List[RawEventsRepo] = Field(\n        alias=\"issueContributionsByRepository\"\n    )\n    pr_contribs_by_repo: List[RawEventsRepo] = Field(\n        alias=\"pullRequestContributionsByRepository\"\n    )\n    review_contribs_by_repo: List[RawEventsRepo] = Field(\n        alias=\"pullRequestReviewContributionsByRepository\"\n    )\n    repo_contribs: RawEventsRepoContribs = Field(alias=\"repositoryContributions\")\n"
  },
  {
    "path": "backend/src/data/github/graphql/user/follows/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/data/github/graphql/user/follows/follows.py",
    "content": "# import json\nfrom typing import Dict, Optional, Union\n\nfrom src.data.github.graphql.template import get_template\nfrom src.data.github.graphql.user.follows.models import RawFollows\n\n\ndef get_user_followers(\n    user_id: str, first: int = 100, after: str = \"\", access_token: Optional[str] = None\n) -> RawFollows:\n    \"\"\"gets user's followers and users following'\"\"\"\n\n    variables: Dict[str, Union[str, int]] = (\n        {\"login\": user_id, \"first\": first, \"after\": after}\n        if after != \"\"\n        else {\"login\": user_id, \"first\": first}\n    )\n\n    query_str: str = (\n        \"\"\"\n        query getUser($login: String!, $first: Int!, $after: String!) {\n            user(login: $login){\n                followers(first: $first, after: $after){\n                    nodes{\n                        name,\n                        login,\n                        url\n                    }\n                    pageInfo{\n                        hasNextPage,\n                        endCursor\n                    }\n                }\n            }\n        }\n    \"\"\"\n        if after != \"\"\n        else \"\"\"\n        query getUser($login: String!, $first: Int!) {\n            user(login: $login){\n                followers(first: $first){\n                    nodes{\n                        name,\n                        login,\n                        url\n                    }\n                    pageInfo{\n                        hasNextPage,\n                        endCursor\n                    }\n                }\n            }\n        }\n    \"\"\"\n    )\n\n    query = {\n        \"variables\": variables,\n        \"query\": query_str,\n    }\n\n    output_dict = get_template(query, access_token)[\"data\"][\"user\"][\"followers\"]\n    return RawFollows.model_validate(output_dict)\n\n\ndef get_user_following(\n    user_id: str, first: int = 10, after: str = \"\", access_token: Optional[str] = None\n) -> RawFollows:\n    \"\"\"gets user's followers and users following'\"\"\"\n\n    variables: Dict[str, Union[str, int]] = (\n        {\"login\": user_id, \"first\": first, \"after\": after}\n        if after != \"\"\n        else {\"login\": user_id, \"first\": first}\n    )\n\n    query_str: str = (\n        \"\"\"\n        query getUser($login: String!, $first: Int!, $after: String!) {\n            user(login: $login){\n                following(first: $first, after: $after){\n                    nodes{\n                        name,\n                        login,\n                        url\n                    }\n                    pageInfo{\n                        hasNextPage,\n                        endCursor\n                    }\n                }\n            }\n        }\n    \"\"\"\n        if after != \"\"\n        else \"\"\"\n        query getUser($login: String!, $first: Int!) {\n            user(login: $login){\n                following(first: $first){\n                    nodes{\n                        name,\n                        login,\n                        url\n                    }\n                    pageInfo{\n                        hasNextPage,\n                        endCursor\n                    }\n                }\n            }\n        }\n    \"\"\"\n    )\n\n    query = {\n        \"variables\": variables,\n        \"query\": query_str,\n    }\n\n    output_dict = get_template(query, access_token)[\"data\"][\"user\"][\"following\"]\n    return RawFollows.model_validate(output_dict)\n"
  },
  {
    "path": "backend/src/data/github/graphql/user/follows/models.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel, Field\n\nfrom src.models import User\n\n\nclass PageInfo(BaseModel):\n    has_next_page: bool = Field(alias=\"hasNextPage\")\n    end_cursor: Optional[str] = Field(alias=\"endCursor\")\n\n    class Config:\n        allow_none = True\n\n\nclass RawFollows(BaseModel):\n    nodes: List[User]\n    page_info: PageInfo = Field(alias=\"pageInfo\")\n"
  },
  {
    "path": "backend/src/data/github/language_map.py",
    "content": "import json\nimport urllib.request\nfrom typing import Any, Dict\n\nBLACKLIST = [\".md\"]\n\nwith urllib.request.urlopen(\n    \"https://raw.githubusercontent.com/blakeembrey/language-map/main/languages.json\"\n) as url:\n    data: Dict[str, Dict[str, Any]] = json.loads(url.read().decode())\n    languages = {\n        k: v\n        for k, v in data.items()\n        if v[\"type\"] in [\"programming\", \"markup\"] and \"color\" in v and \"extensions\" in v\n    }\n    extensions: Dict[str, Dict[str, str]] = {}\n    for lang_name, lang in languages.items():\n        for extension in lang[\"extensions\"]:\n            if extension not in BLACKLIST:\n                extensions[extension] = {\"color\": lang[\"color\"], \"name\": lang_name}\n    extensions = dict(sorted(extensions.items(), key=lambda x: x[0]))\n    extensions[\".tsx\"][\"name\"] = \"TypeScript\"\n    extensions[\".tsx\"][\"color\"] = \"#2B7489\"\n    extensions[\".cs\"][\"name\"] = \"C#\"\n    extensions[\".cs\"][\"color\"] = \"#178600\"\n    extensions[\".ml\"][\"name\"] = \"OCaml\"\n    extensions[\".ml\"][\"color\"] = \"#3BE133\"\n\n    with open(\"src/data/github/extensions.json\", \"w\") as f:\n        json.dump(extensions, f, indent=4)\n"
  },
  {
    "path": "backend/src/data/github/rest/__init__.py",
    "content": "from src.data.github.rest.commit import get_commit_files\nfrom src.data.github.rest.models import RawCommit, RawCommitFile\nfrom src.data.github.rest.repo import get_repo_commits, get_repo_stargazers\nfrom src.data.github.rest.template import RESTError, RESTErrorNotFound\nfrom src.data.github.rest.user import get_user, get_user_starred_repos\n\n__all__ = [\n    \"get_commit_files\",\n    \"RawCommit\",\n    \"RawCommitFile\",\n    \"get_repo_commits\",\n    \"get_repo_stargazers\",\n    \"RESTError\",\n    \"RESTErrorNotFound\",\n    \"get_user\",\n    \"get_user_starred_repos\",\n]\n"
  },
  {
    "path": "backend/src/data/github/rest/commit.py",
    "content": "from typing import List, Optional\n\nfrom src.data.github.rest.models import RawCommitFile\nfrom src.data.github.rest.template import get_template\n\nBASE_URL = \"https://api.github.com/repos/\"\n\n\ndef get_commit_files(\n    owner: str, repo: str, sha: str, access_token: Optional[str] = None\n) -> Optional[List[RawCommitFile]]:\n    \"\"\"\n    Returns raw repository data\n    :param owner: repository owner\n    :param repo: repository name\n    :param sha: commit sha\n    :param access_token: GitHub access token\n    :return: repository data\n    \"\"\"\n\n    try:\n        output = get_template(\n            BASE_URL + owner + \"/\" + repo + \"/commits/\" + sha, access_token\n        )\n        files = output[\"files\"]\n        return [RawCommitFile.model_validate(f) for f in files]\n    except Exception:\n        return None\n"
  },
  {
    "path": "backend/src/data/github/rest/models.py",
    "content": "from datetime import datetime\n\nfrom pydantic import BaseModel\n\n\nclass RawCommit(BaseModel):\n    timestamp: datetime\n    node_id: str\n\n\nclass RawCommitFile(BaseModel):\n    filename: str\n    additions: int\n    deletions: int\n"
  },
  {
    "path": "backend/src/data/github/rest/repo.py",
    "content": "import logging\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom src.data.github.rest.models import RawCommit\nfrom src.data.github.rest.template import RESTError, get_template, get_template_plural\n\nBASE_URL = \"https://api.github.com/repos/\"\n\n\n# NOTE: unused, untested\ndef get_repo(access_token: str, owner: str, repo: str) -> Dict[str, Any]:\n    \"\"\"\n    Returns raw repository data\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :return: repository data\n    \"\"\"\n    return get_template(BASE_URL + owner + \"/\" + repo, access_token)\n\n\n# NOTE: unused, untested\ndef get_repo_languages(\n    access_token: str, owner: str, repo: str\n) -> List[Dict[str, Any]]:\n    \"\"\"\n    Returns repository language breakdown\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :return: repository language breakdown\n    \"\"\"\n    return get_template_plural(\n        BASE_URL + owner + \"/\" + repo + \"/languages\", access_token\n    )\n\n\ndef get_repo_stargazers(\n    access_token: str, owner: str, repo: str, per_page: int = 100, page: int = 1\n) -> List[Dict[str, Any]]:\n    \"\"\"\n    Returns stargazers with timestamp for repository\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :param per_page: number of items per page\n    :param page: page number\n    :return: stargazers with timestamp for repository\n    \"\"\"\n    return get_template_plural(\n        BASE_URL + owner + \"/\" + repo + \"/stargazers\",\n        access_token,\n        per_page=per_page,\n        page=page,\n        accept_header=\"applicaiton/vnd.github.v3.star+json\",\n    )\n\n\n# NOTE: unused, untested\n# does not accept per page, exceeds if necessary\ndef get_repo_code_frequency(access_token: str, owner: str, repo: str) -> Dict[str, Any]:\n    \"\"\"\n    Returns code frequency for repository\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :return: code frequency for repository\n    \"\"\"\n    return get_template(\n        BASE_URL + owner + \"/\" + repo + \"/stats/code_frequency\", access_token\n    )\n\n\n# NOTE: unused, untested\ndef get_repo_commit_activity(\n    access_token: str, owner: str, repo: str\n) -> Dict[str, Any]:\n    \"\"\"\n    Returns commit activity for past year, broken by week\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :return: commit activity for past year, broken by week\n    \"\"\"\n    return get_template(\n        BASE_URL + owner + \"/\" + repo + \"/stats/commit_activity\", access_token\n    )\n\n\n# NOTE: unused, untested\ndef get_repo_contributors(access_token: str, owner: str, repo: str) -> Dict[str, Any]:\n    \"\"\"\n    Returns contributors for a repository\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :return: contributors for a repository\n    \"\"\"\n    return get_template(\n        BASE_URL + owner + \"/\" + repo + \"/stats/contributors\", access_token\n    )\n\n\n# NOTE: unused, untested\ndef get_repo_weekly_commits(access_token: str, owner: str, repo: str) -> Dict[str, Any]:\n    \"\"\"\n    Returns contributions by week, owner/non-owner\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :return: contributions by week, owner/non-owner\n    \"\"\"\n    return get_template(\n        BASE_URL + owner + \"/\" + repo + \"/stats/participation\", access_token\n    )\n\n\n# NOTE: unused, untested\ndef get_repo_hourly_commits(access_token: str, owner: str, repo: str) -> Dict[str, Any]:\n    \"\"\"\n    Returns contributions by day, hour for repository\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    \"\"\"\n    return get_template(\n        BASE_URL + owner + \"/\" + repo + \"/stats/punch_card\", access_token\n    )\n\n\ndef get_repo_commits(\n    owner: str,\n    repo: str,\n    user: Optional[str] = None,\n    since: Optional[datetime] = None,\n    until: Optional[datetime] = None,\n    page: int = 1,\n    access_token: Optional[str] = None,\n) -> List[RawCommit]:\n    \"\"\"\n    Returns most recent commits\n    :param access_token: GitHub access token\n    :param owner: repository owner\n    :param repo: repository name\n    :param user: optional GitHub user if not owner\n    :param since: optional datetime to start from\n    :param until: optional datetime to end at\n    :param page: optional page number\n    :return: Up to 100 commits from page\n    \"\"\"\n    user = user if user is not None else owner\n    query = BASE_URL + owner + \"/\" + repo + \"/commits?author=\" + user\n    if since is not None:\n        query += f\"&since={str(since)}\"\n    if until is not None:\n        query += f\"&until={str(until)}\"\n    try:\n        data = get_template_plural(query, access_token, page=page)\n\n        def extract_info(x: Any) -> RawCommit:\n            dt = x[\"commit\"][\"committer\"][\"date\"]\n            temp = {\n                \"timestamp\": datetime.strptime(dt, \"%Y-%m-%dT%H:%M:%SZ\"),\n                \"node_id\": x[\"node_id\"],\n            }\n            return RawCommit.model_validate(temp)\n\n        return list(map(extract_info, data))\n    except RESTError:\n        return []\n    except Exception as e:\n        logging.exception(e)\n        return []\n"
  },
  {
    "path": "backend/src/data/github/rest/template.py",
    "content": "from datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nimport requests\nfrom requests.exceptions import ReadTimeout\n\nfrom src.constants import TIMEOUT\nfrom src.data.github.utils import get_access_token\n\ns = requests.session()\n\n\nclass RESTError(Exception):\n    pass\n\n\nclass RESTErrorUnauthorized(RESTError):\n    pass\n\n\nclass RESTErrorNotFound(RESTError):\n    pass\n\n\nclass RESTErrorEmptyRepo(RESTError):\n    pass\n\n\nclass RESTErrorTimeout(RESTError):\n    pass\n\n\ndef _get_template(\n    query: str,\n    params: Dict[str, Any],\n    accept_header: str,\n    access_token: Optional[str] = None,\n    retries: int = 0,\n) -> Any:\n    \"\"\"\n    Internal template for interacting with the GitHub REST API\n    :param query: The query to be sent to the GitHub API\n    :param params: The parameters to be sent to the GitHub API\n    :param access_token: The access token to be sent to the GitHub API\n    :param accept_header: The accept header to be sent to the GitHub API\n    :return: The response from the GitHub API\n    \"\"\"\n    start = datetime.now()\n\n    new_access_token = get_access_token(access_token)\n    headers: Dict[str, str] = {\n        \"Accept\": accept_header,\n        \"Authorization\": f\"bearer {new_access_token}\",\n    }\n\n    try:\n        r = s.get(query, params=params, headers=headers, timeout=TIMEOUT)\n    except ReadTimeout:\n        raise RESTErrorTimeout(\"REST Error: Request Timeout\")\n\n    if r.status_code == 200:\n        print(\"REST API\", new_access_token, datetime.now() - start)\n        return r.json()\n\n    if r.status_code == 401:\n        raise RESTErrorUnauthorized(\"REST Error: Unauthorized\")\n\n    if r.status_code == 404:\n        raise RESTErrorNotFound(\"REST Error: Not Found\")\n\n    if r.status_code == 409:\n        raise RESTErrorEmptyRepo(\"REST Error: Empty Repository\")\n\n    if retries < 3:\n        print(\"REST Error, Retrying:\", new_access_token)\n        return _get_template(query, params, accept_header, access_token, retries + 1)\n    raise RESTError(f\"REST Error: {str(r.status_code)}\")\n\n\ndef get_template(\n    query: str,\n    access_token: Optional[str] = None,\n    accept_header: str = \"application/vnd.github.v3+json\",\n) -> Dict[str, Any]:\n    \"\"\"\n    Template for interacting with the GitHub REST API (singular)\n    :param query: The query to be sent to the GitHub API\n    :param access_token: The access token to be sent to the GitHub API\n    :param accept_header: The accept header to be sent to the GitHub API\n    :return: The response from the GitHub API\n    \"\"\"\n    return _get_template(query, {}, accept_header, access_token)\n\n\ndef get_template_plural(\n    query: str,\n    access_token: Optional[str] = None,\n    per_page: int = 100,\n    page: int = 1,\n    accept_header: str = \"application/vnd.github.v3+json\",\n) -> List[Dict[str, Any]]:\n    \"\"\"\n    Template for interacting with the GitHub REST API (plural)\n    :param query: The query to be sent to the GitHub API\n    :param access_token: The access token to be sent to the GitHub API\n    :param per_page: The number of items to be returned per page\n    :param page: The page number to be returned\n    :param accept_header: The accept header to be sent to the GitHub API\n    :return: The response from the GitHub API\n    \"\"\"\n    params: Dict[str, str] = {\"per_page\": str(per_page), \"page\": str(page)}\n    return _get_template(query, params, accept_header, access_token)\n"
  },
  {
    "path": "backend/src/data/github/rest/user.py",
    "content": "from typing import Any, Dict, List\n\nfrom src.data.github.rest.template import get_template, get_template_plural\n\nBASE_URL = \"https://api.github.com/users/\"\n\n\ndef get_user(user_id: str, access_token: str) -> Dict[str, Any]:\n    \"\"\"\n    Returns raw user data\n    :param user_id: GitHub user id\n    :param access_token: GitHub access token\n    \"\"\"\n    return get_template(BASE_URL + user_id, access_token)\n\n\ndef get_user_starred_repos(\n    user_id: str, access_token: str, per_page: int = 100, page: int = 1\n) -> List[Dict[str, Any]]:\n    \"\"\"\n    Returns list of starred repos\n    :param user_id: GitHub user id\n    :param access_token: GitHub access token\n    :param per_page: number of repos to return per page\n    \"\"\"\n    return get_template_plural(\n        BASE_URL + user_id + \"/starred\",\n        access_token,\n        per_page=per_page,\n        page=page,\n        accept_header=\"application/vnd.github.v3.star+json\",\n    )\n"
  },
  {
    "path": "backend/src/data/github/utils.py",
    "content": "from typing import Optional\n\nfrom src.data.mongo.secret import get_random_key\n\n\ndef get_access_token(access_token: Optional[str] = None) -> str:\n    return access_token if access_token is not None else get_random_key()\n"
  },
  {
    "path": "backend/src/data/mongo/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/data/mongo/main.py",
    "content": "from motor.core import AgnosticCollection\nfrom motor.motor_asyncio import AsyncIOMotorClient\n\nfrom src.constants import LOCAL, MONGODB_PASSWORD, PROD\n\n\ndef get_conn_str(password: str, database: str) -> str:\n    return f\"mongodb+srv://root:{password}@backend2.e50j8dp.mongodb.net/{database}?retryWrites=true&w=majority\"\n\n\nif LOCAL:\n    DB = None\nelif PROD:\n    conn_str = get_conn_str(MONGODB_PASSWORD, \"prod_backend\")\n    CLIENT = AsyncIOMotorClient(\n        conn_str, serverSelectionTimeoutMS=5000, tlsInsecure=True\n    )\n    DB = CLIENT.prod_backend  # type: ignore\nelse:\n    conn_str = get_conn_str(MONGODB_PASSWORD, \"dev_backend\")\n    CLIENT = AsyncIOMotorClient(  # type: ignore\n        conn_str, serverSelectionTimeoutMS=5000, tlsInsecure=True\n    )\n    DB = CLIENT.dev_backend  # type: ignore\n\n# Overwrite type since only None if Local=True\nSECRETS: AgnosticCollection = None if DB is None else DB.secrets  # type: ignore\nUSERS: AgnosticCollection = None if DB is None else DB.users  # type: ignore\nUSER_MONTHS: AgnosticCollection = None if DB is None else DB.user_months  # type: ignore\n"
  },
  {
    "path": "backend/src/data/mongo/secret/__init__.py",
    "content": "from src.data.mongo.secret.functions import get_random_key, update_keys\n\n__all__ = [\"get_random_key\", \"update_keys\"]\n"
  },
  {
    "path": "backend/src/data/mongo/secret/functions.py",
    "content": "from datetime import timedelta\nfrom random import randint\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom src.constants import TEST_TOKEN\nfrom src.data.mongo.main import SECRETS\nfrom src.data.mongo.secret.models import SecretModel\nfrom src.utils import alru_cache\n\n\n@alru_cache(ttl=timedelta(minutes=15))\nasync def get_keys(no_cache: bool = False) -> Tuple[bool, List[str]]:\n    secrets: Optional[Dict[str, Any]] = await SECRETS.find_one({\"project\": \"main\"})\n    if secrets is None:\n        return (False, [])\n\n    tokens = SecretModel.model_validate(secrets).access_tokens\n    return (True, tokens)\n\n\nsecret_keys: List[str] = []\n\n\nasync def update_keys(no_cache: bool = False) -> None:\n    global secret_keys\n    secret_keys = await get_keys(no_cache=no_cache)\n\n\ndef get_random_key() -> str:\n    global secret_keys\n    if len(secret_keys) == 0:\n        return TEST_TOKEN\n\n    return secret_keys[randint(0, len(secret_keys) - 1)]\n"
  },
  {
    "path": "backend/src/data/mongo/secret/models.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\n\n\nclass SecretModel(BaseModel):\n    project: str\n    access_tokens: List[str]\n"
  },
  {
    "path": "backend/src/data/mongo/user/__init__.py",
    "content": "from src.data.mongo.user.functions import delete_user, is_user_key, update_user\nfrom src.data.mongo.user.get import get_full_user, get_public_user\nfrom src.data.mongo.user.models import FullUserModel, PublicUserModel\n\n__all__ = [\n    \"delete_user\",\n    \"is_user_key\",\n    \"update_user\",\n    \"get_full_user\",\n    \"get_public_user\",\n    \"FullUserModel\",\n    \"PublicUserModel\",\n]\n"
  },
  {
    "path": "backend/src/data/mongo/user/functions.py",
    "content": "from typing import Any, Dict, Optional\n\nfrom src.data.mongo.main import USERS\n\n\nasync def is_user_key(user_id: str, user_key: str) -> bool:\n    user: Optional[dict[str, str]] = await USERS.find_one(\n        {\"user_id\": user_id}, {\"user_key\": 1}\n    )\n    return user is not None and user.get(\"user_key\", \"\") == user_key\n\n\nasync def update_user(user_id: str, raw_user: Dict[str, Any]) -> None:\n    await USERS.update_one(\n        {\"user_id\": user_id},\n        {\"$set\": raw_user},\n        upsert=True,\n    )\n\n\nasync def delete_user(user_id: str, user_key: str, use_user_key: bool = True) -> bool:\n    if use_user_key:\n        is_key = await is_user_key(user_id, user_key)\n        if not is_key:\n            return False\n\n    await USERS.delete_one({\"user_id\": user_id})\n    return True\n"
  },
  {
    "path": "backend/src/data/mongo/user/get.py",
    "content": "from typing import Any, Dict, Optional, Tuple\n\nfrom pydantic import ValidationError\n\nfrom src.data.mongo.main import USERS\nfrom src.data.mongo.user.models import FullUserModel, PublicUserModel\nfrom src.utils import alru_cache\n\n\n@alru_cache()\nasync def get_public_user(\n    user_id: str, no_cache: bool = False\n) -> Tuple[bool, Optional[PublicUserModel]]:\n    user: Optional[Dict[str, Any]] = await USERS.find_one({\"user_id\": user_id})\n    if user is None:\n        # flag is false, don't cache\n        return (False, None)\n    try:\n        return (True, PublicUserModel.model_validate(user))\n    except (TypeError, KeyError, ValidationError):\n        return (False, None)\n\n\n@alru_cache()\nasync def get_full_user(\n    user_id: str, no_cache: bool = False\n) -> Tuple[bool, Optional[FullUserModel]]:\n    user: Optional[Dict[str, Any]] = await USERS.find_one({\"user_id\": user_id})\n\n    if user is None:\n        # flag is false, don't cache\n        return (False, None)\n\n    try:\n        return (True, FullUserModel.model_validate(user))\n    except (TypeError, KeyError, ValidationError):\n        return (False, None)\n"
  },
  {
    "path": "backend/src/data/mongo/user/models.py",
    "content": "from typing import Optional\n\nfrom pydantic import BaseModel, validator\n\n\nclass PublicUserModel(BaseModel):\n    user_id: str\n    access_token: str\n    private_access: Optional[bool]\n\n    class Config:\n        from_attributes = True\n        validate_assignment = True\n\n    @validator(\"private_access\", pre=True, always=True)\n    def set_name(cls, private_access: Optional[bool]):\n        return False if private_access is None else private_access\n\n\nclass FullUserModel(PublicUserModel):\n    user_key: Optional[str]\n"
  },
  {
    "path": "backend/src/data/mongo/user_months/__init__.py",
    "content": "from src.data.mongo.user_months.functions import set_user_month\nfrom src.data.mongo.user_months.get import get_user_months\nfrom src.data.mongo.user_months.models import UserMonth\n\n__all__ = [\"set_user_month\", \"get_user_months\", \"UserMonth\"]\n"
  },
  {
    "path": "backend/src/data/mongo/user_months/functions.py",
    "content": "from src.data.mongo.main import USER_MONTHS\nfrom src.data.mongo.user_months.models import UserMonth\n\n\nasync def set_user_month(user_month: UserMonth):\n    compressed_user_month = user_month.model_dump()\n    compressed_user_month[\"data\"] = user_month.data.compress()\n\n    await USER_MONTHS.update_one(\n        {\"user_id\": user_month.user_id, \"month\": user_month.month},\n        {\"$set\": compressed_user_month},\n        upsert=True,\n    )\n"
  },
  {
    "path": "backend/src/data/mongo/user_months/get.py",
    "content": "from datetime import date, datetime\nfrom typing import Any, Dict, List\n\nfrom src.constants import API_VERSION, USER_WHITELIST\nfrom src.data.mongo.main import USER_MONTHS\nfrom src.data.mongo.user_months.models import UserMonth\nfrom src.models import UserPackage\n\n\nasync def get_user_months(\n    user_id: str, private_access: bool, start_month: date, end_month: date\n) -> List[UserMonth]:\n    start = datetime(start_month.year, start_month.month, 1)\n    end = datetime(end_month.year, end_month.month, 28)\n    today = datetime.now()\n\n    filters = {\n        \"user_id\": user_id,\n        \"month\": {\"$gte\": start, \"$lte\": end},\n        \"version\": API_VERSION,\n    }\n\n    if private_access:\n        filters[\"private\"] = True\n\n    months: List[Dict[str, Any]] = await USER_MONTHS.find(filters).to_list(length=None)  # type: ignore\n\n    months_data: List[UserMonth] = []\n    for month in months:\n        date_obj: datetime = month[\"month\"]\n        complete = (\n            not (date_obj.year == today.year and date_obj.month == today.month)\n            or user_id in USER_WHITELIST\n        )\n        try:\n            data = UserPackage.decompress(month[\"data\"])\n            months_data.append(\n                UserMonth.model_validate(\n                    {\n                        \"user_id\": user_id,\n                        \"month\": month[\"month\"],\n                        \"version\": API_VERSION,\n                        \"private\": month[\"private\"],\n                        \"complete\": complete,\n                        \"data\": data.model_dump(),\n                    }\n                )\n            )\n        except Exception:\n            pass\n\n    return months_data\n"
  },
  {
    "path": "backend/src/data/mongo/user_months/models.py",
    "content": "from datetime import datetime\n\nfrom pydantic import BaseModel\n\nfrom src.models import UserPackage\n\n\nclass UserMonth(BaseModel):\n    user_id: str\n    month: datetime\n    version: float\n    private: bool\n    complete: bool\n    data: UserPackage\n"
  },
  {
    "path": "backend/src/main.py",
    "content": "from typing import Dict\n\nimport sentry_sdk\nfrom dotenv import find_dotenv, load_dotenv\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom sentry_sdk.integrations.asgi import SentryAsgiMiddleware\n\nload_dotenv(find_dotenv())\n\n# flake8: noqa E402\n\n# add endpoints here (after load dotenv)\nfrom src.constants import PROD, SENTRY_DSN\nfrom src.routers import (\n    asset_router,\n    auth_router,\n    dev_router,\n    user_router,\n    wrapped_router,\n)\n\n\"\"\"\nSETUP\n\"\"\"\n\napp = FastAPI()\n\norigins = [\n    \"http://localhost:3000\",\n    \"http://localhost:3001\",\n    \"https://githubtrends.io\",\n    \"https://www.githubtrends.io\",\n    \"https://githubwrapped.io\",\n    \"https://www.githubwrapped.io\",\n]\n\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=origins,\n    allow_credentials=True,\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n\nsentry_sdk.init(\n    SENTRY_DSN,\n    traces_sample_rate=(1.0 if PROD else 1.0),\n)\n\napp.add_middleware(\n    SentryAsgiMiddleware,\n)\n\n\n@app.get(\"/\")\nasync def read_root() -> Dict[str, str]:\n    return {\"Hello\": \"World\"}\n\n\n@app.get(\"/info\")\ndef get_info() -> Dict[str, bool]:\n    return {\"PROD\": PROD}\n\n\napp.include_router(asset_router, prefix=\"/assets\", tags=[\"Assets\"])\napp.include_router(auth_router, prefix=\"/auth\", tags=[\"Auth\"])\napp.include_router(dev_router, prefix=\"/dev\", tags=[\"Dev\"])\napp.include_router(user_router, prefix=\"/user\", tags=[\"Users\"])\napp.include_router(wrapped_router, prefix=\"/wrapped\", tags=[\"Wrapped\"])\n"
  },
  {
    "path": "backend/src/models/__init__.py",
    "content": "from src.models.user.contribs import (\n    ContributionDay,\n    Language,\n    RepoContributionStats,\n    UserContributions,\n)\nfrom src.models.user.follows import User, UserFollows\nfrom src.models.user.main import UserPackage\nfrom src.models.wrapped.calendar import (\n    CalendarData,\n    CalendarDayDatum,\n    CalendarLanguageDayDatum,\n)\nfrom src.models.wrapped.langs import LangData, LangDatum\nfrom src.models.wrapped.main import WrappedPackage\nfrom src.models.wrapped.numeric import ContribStats, LOCStats, MiscStats, NumericData\nfrom src.models.wrapped.repos import RepoData, RepoDatum\nfrom src.models.wrapped.time import DayData, MonthData, TimeDatum\nfrom src.models.wrapped.timestamps import TimestampData, TimestampDatum\n\n__all__ = [\n    \"ContributionDay\",\n    \"Language\",\n    \"RepoContributionStats\",\n    \"UserContributions\",\n    \"User\",\n    \"UserFollows\",\n    \"UserPackage\",\n    \"CalendarData\",\n    \"CalendarDayDatum\",\n    \"CalendarLanguageDayDatum\",\n    \"LangData\",\n    \"LangDatum\",\n    \"WrappedPackage\",\n    \"ContribStats\",\n    \"LOCStats\",\n    \"MiscStats\",\n    \"NumericData\",\n    \"RepoData\",\n    \"RepoDatum\",\n    \"DayData\",\n    \"MonthData\",\n    \"TimeDatum\",\n    \"TimestampData\",\n    \"TimestampDatum\",\n]\n"
  },
  {
    "path": "backend/src/models/background.py",
    "content": "from datetime import date\nfrom typing import Optional\n\nfrom pydantic import BaseModel\n\n\nclass UpdateUserBackgroundTask(BaseModel):\n    user_id: str\n    access_token: Optional[str]\n    private_access: bool\n    start_date: Optional[date]\n    end_date: Optional[date]\n"
  },
  {
    "path": "backend/src/models/svg.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel\n\n\nclass LanguageStats(BaseModel):\n    lang: str\n    loc: int\n    percent: float\n    color: Optional[str]\n\n\nclass RepoLanguage(BaseModel):\n    lang: str\n    color: Optional[str]\n    loc: int\n\n\nclass RepoStats(BaseModel):\n    repo: str\n    private: bool\n    langs: List[RepoLanguage]\n    loc: int\n"
  },
  {
    "path": "backend/src/models/user/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/models/user/contribs.py",
    "content": "from datetime import date, datetime\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom pydantic import BaseModel\n\n\nclass Language(BaseModel):\n    color: Optional[str]\n    additions: int\n    deletions: int\n\n    def compress(self) -> List[Any]:\n        return [self.color, self.additions, self.deletions]\n\n    @classmethod\n    def decompress(cls, data: List[Any]) -> \"Language\":\n        return Language(color=data[0], additions=data[1], deletions=data[2])\n\n    def __add__(self, other: \"Language\") -> \"Language\":\n        return Language(\n            color=self.color,\n            additions=self.additions + other.additions,\n            deletions=self.deletions + other.deletions,\n        )\n\n\nclass ContributionStats(BaseModel):\n    contribs_count: int\n    commits_count: int\n    issues_count: int\n    prs_count: int\n    reviews_count: int\n    repos_count: int\n    other_count: int\n    languages: Dict[str, Language]\n\n    def compress(self) -> List[Any]:\n        out: List[Any] = [\n            [\n                self.contribs_count,\n                self.commits_count,\n                self.issues_count,\n                self.prs_count,\n                self.reviews_count,\n                self.repos_count,\n                self.other_count,\n            ],\n            *[[name] + stats.compress() for name, stats in self.languages.items()],\n        ]\n\n        return out\n\n    @classmethod\n    def decompress(cls, data: List[Any]) -> \"ContributionStats\":\n        return ContributionStats(\n            contribs_count=data[0][0],\n            commits_count=data[0][1],\n            issues_count=data[0][2],\n            prs_count=data[0][3],\n            reviews_count=data[0][4],\n            repos_count=data[0][5],\n            other_count=data[0][6],\n            languages={x[0]: Language.decompress(x[1:]) for x in data[1:]},\n        )\n\n    def __add__(self, other: \"ContributionStats\") -> \"ContributionStats\":\n        languages = self.languages\n        for lang, lang_obj in other.languages.items():\n            if lang in languages:\n                languages[lang] += lang_obj\n            else:\n                languages[lang] = lang_obj\n\n        return ContributionStats(\n            contribs_count=self.contribs_count + other.contribs_count,\n            commits_count=self.commits_count + other.commits_count,\n            issues_count=self.issues_count + other.issues_count,\n            prs_count=self.prs_count + other.prs_count,\n            reviews_count=self.reviews_count + other.reviews_count,\n            repos_count=self.repos_count + other.repos_count,\n            other_count=self.other_count + other.other_count,\n            languages=languages,\n        )\n\n    @classmethod\n    def empty(cls) -> \"ContributionStats\":\n        return ContributionStats(\n            contribs_count=0,\n            commits_count=0,\n            issues_count=0,\n            prs_count=0,\n            reviews_count=0,\n            repos_count=0,\n            other_count=0,\n            languages={},\n        )\n\n\nclass ContributionLists(BaseModel):\n    commits: List[datetime]\n    issues: List[datetime]\n    prs: List[datetime]\n    reviews: List[datetime]\n    repos: List[datetime]\n\n    def compress(self) -> List[Any]:\n        return [self.commits, self.issues, self.prs, self.reviews, self.repos]\n\n    @classmethod\n    def decompress(cls, data: List[Any]) -> \"ContributionLists\":\n        return ContributionLists(\n            commits=data[0], issues=data[1], prs=data[2], reviews=data[3], repos=data[4]\n        )\n\n\nclass ContributionDay(BaseModel):\n    date: str\n    weekday: int\n    stats: ContributionStats\n    lists: ContributionLists\n\n    def compress(self) -> List[Any]:\n        return [\n            self.date,\n            self.weekday,\n            self.stats.compress(),\n            self.lists.compress(),\n        ]\n\n    @classmethod\n    def decompress(cls, data: List[Any]) -> \"ContributionDay\":\n        return ContributionDay(\n            date=data[0],\n            weekday=data[1],\n            stats=ContributionStats.decompress(data[2]),\n            lists=ContributionLists.decompress(data[3]),\n        )\n\n\nclass RepoContributionStats(ContributionStats, BaseModel):\n    private: bool\n    contribs_count: int\n    commits_count: int\n    issues_count: int\n    prs_count: int\n    reviews_count: int\n    repos_count: int\n    other_count: int\n    languages: Dict[str, Language]\n\n    def compress(self) -> List[Any]:\n        out = super().compress()\n        out[0].append(self.private)\n        return out\n\n    @classmethod\n    def decompress(cls, data: List[Any]) -> \"RepoContributionStats\":\n        contribs = super().decompress(data).model_dump()\n        contribs[\"private\"] = data[0][7]\n        return RepoContributionStats(**contribs)\n\n    def __add__(  # type: ignore\n        self, other: \"RepoContributionStats\"\n    ) -> \"RepoContributionStats\":\n        new_self = ContributionStats(**self.model_dump())\n        new_other = ContributionStats(**other.model_dump())\n        combined = (new_self + new_other).model_dump()\n        combined[\"private\"] = self.private\n        return RepoContributionStats(**combined)\n\n\nclass UserContributions(BaseModel):\n    total_stats: ContributionStats\n    public_stats: ContributionStats\n    total: List[ContributionDay]\n    public: List[ContributionDay]\n    repo_stats: Dict[str, RepoContributionStats]\n    repos: Dict[str, List[ContributionDay]]\n\n    def compress(self) -> List[Any]:\n        new_total_stats = self.total_stats.compress()\n        new_public_stats = self.public_stats.compress()\n        new_total = [x.compress() for x in self.total]\n        new_public = [x.compress() for x in self.public]\n        new_repo_stats = {k: v.compress() for k, v in self.repo_stats.items()}\n        new_repos = {k: [x.compress() for x in v] for k, v in self.repos.items()}\n\n        return [\n            new_total_stats,\n            new_public_stats,\n            new_total,\n            new_public,\n            new_repo_stats,\n            new_repos,\n        ]\n\n    @classmethod\n    def decompress(cls, data: List[Any]) -> \"UserContributions\":\n        total_stats = ContributionStats.decompress(data[0])\n        public_stats = ContributionStats.decompress(data[1])\n        total = [ContributionDay.decompress(x) for x in data[2]]\n        public = [ContributionDay.decompress(x) for x in data[3]]\n        repo_stats = {\n            k: RepoContributionStats.decompress(v) for k, v in data[4].items()\n        }\n        repos = {\n            k: [ContributionDay.decompress(x) for x in v] for k, v in data[5].items()\n        }\n\n        return UserContributions(\n            total_stats=total_stats,\n            public_stats=public_stats,\n            total=total,\n            public=public,\n            repo_stats=repo_stats,\n            repos=repos,\n        )\n\n    def __add__(self, other: \"UserContributions\") -> \"UserContributions\":\n        new_total_stats = self.total_stats + other.total_stats\n        new_public_stats = self.public_stats + other.public_stats\n        new_total = sorted(self.total + other.total, key=lambda x: x.date)\n        new_public = sorted(self.public + other.public, key=lambda x: x.date)\n        new_repo_stats = self.repo_stats\n        for repo, stats in other.repo_stats.items():\n            if repo in new_repo_stats:\n                new_repo_stats[repo] += stats\n            else:\n                new_repo_stats[repo] = stats\n        new_repos = self.repos\n        for repo, days in other.repos.items():\n            if repo in new_repos:\n                new_repos[repo] = sorted(new_repos[repo] + days, key=lambda x: x.date)\n            else:\n                new_repos[repo] = days\n\n        return UserContributions(\n            total_stats=new_total_stats,\n            public_stats=new_public_stats,\n            total=new_total,\n            public=new_public,\n            repo_stats=new_repo_stats,\n            repos=new_repos,\n        )\n\n    @staticmethod\n    def trim_contribs(\n        contribs: List[ContributionDay], start_date: date, end_date: date\n    ) -> Tuple[List[ContributionDay], ContributionStats]:\n        new_total: List[ContributionDay] = []\n        for day in contribs:\n            curr_date = datetime.strptime(day.date, \"%Y-%m-%d\").date()\n            if curr_date >= start_date and curr_date <= end_date:\n                new_total.append(day)\n\n        new_languages: Dict[str, Language] = {}\n        for day in new_total:\n            for lang in day.stats.languages:\n                if lang in new_languages:\n                    new_languages[lang] += day.stats.languages[lang]\n                else:\n                    new_languages[lang] = day.stats.languages[lang]\n\n        new_stats = ContributionStats(\n            contribs_count=sum(x.stats.contribs_count for x in new_total),\n            commits_count=sum(x.stats.commits_count for x in new_total),\n            issues_count=sum(x.stats.issues_count for x in new_total),\n            prs_count=sum(x.stats.prs_count for x in new_total),\n            reviews_count=sum(x.stats.reviews_count for x in new_total),\n            repos_count=sum(x.stats.repos_count for x in new_total),\n            other_count=sum(x.stats.other_count for x in new_total),\n            languages=new_languages,\n        )\n\n        return new_total, new_stats\n\n    def trim(self, start: date, end: date) -> \"UserContributions\":\n        new_total, new_total_stats = self.trim_contribs(self.total, start, end)\n        new_public, new_public_stats = self.trim_contribs(self.public, start, end)\n\n        new_repos_dict: Dict[str, List[ContributionDay]] = {}\n        new_repo_stats_dict: Dict[str, RepoContributionStats] = {}\n        for repo_name, repo in self.repos.items():\n            new_repo_total, _new_repo_stats = self.trim_contribs(repo, start, end)\n            if len(new_repo_total) > 0:\n                new_repos_dict[repo_name] = new_repo_total\n                raw_new_repo_stats = _new_repo_stats.model_dump()\n                raw_new_repo_stats[\"private\"] = self.repo_stats[repo_name].private\n                new_repo_stats = RepoContributionStats(**raw_new_repo_stats)\n                new_repo_stats_dict[repo_name] = new_repo_stats\n\n        return UserContributions(\n            total_stats=new_total_stats,\n            public_stats=new_public_stats,\n            total=new_total,\n            public=new_public,\n            repo_stats=new_repo_stats_dict,\n            repos=new_repos_dict,\n        )\n\n    @classmethod\n    def empty(cls) -> \"UserContributions\":\n        return UserContributions(\n            total_stats=ContributionStats.empty(),\n            public_stats=ContributionStats.empty(),\n            total=[],\n            public=[],\n            repo_stats={},\n            repos={},\n        )\n"
  },
  {
    "path": "backend/src/models/user/follows.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel\n\n\nclass User(BaseModel):\n    name: Optional[str]\n    login: str\n    url: str\n\n    class Config:\n        allow_none = True\n\n\nclass UserFollows(BaseModel):\n    followers: List[User]\n    following: List[User]\n"
  },
  {
    "path": "backend/src/models/user/main.py",
    "content": "from datetime import date\nfrom typing import Any, Dict\n\nfrom pydantic import BaseModel\n\nfrom src.models.user.contribs import UserContributions\n\n# from src.models.user.follows import UserFollows\n\n\nclass UserPackage(BaseModel):\n    contribs: UserContributions\n    incomplete: bool = False\n\n    def compress(self):\n        return {\n            \"c\": self.contribs.compress(),\n        }\n\n    @classmethod\n    def decompress(cls, data: Dict[str, Any]) -> \"UserPackage\":\n        return UserPackage(\n            contribs=UserContributions.decompress(data[\"c\"]),\n        )\n\n    def __add__(self, other: \"UserPackage\") -> \"UserPackage\":\n        return UserPackage(contribs=self.contribs + other.contribs)\n\n    def trim(self, start: date, end: date) -> \"UserPackage\":\n        return UserPackage(contribs=self.contribs.trim(start, end))\n\n    @classmethod\n    def empty(cls) -> \"UserPackage\":\n        return UserPackage(contribs=UserContributions.empty())\n"
  },
  {
    "path": "backend/src/models/wrapped/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/models/wrapped/calendar.py",
    "content": "from typing import Dict, List\n\nfrom pydantic import BaseModel\n\n\nclass CalendarLanguageDayDatum(BaseModel):\n    loc_added: int\n    loc_changed: int\n\n\nclass CalendarDayDatum(BaseModel):\n    day: str\n    contribs: int\n    commits: int\n    issues: int\n    prs: int\n    reviews: int\n    loc_added: int\n    loc_changed: int\n    top_langs: Dict[str, CalendarLanguageDayDatum]\n\n\nclass CalendarData(BaseModel):\n    days: List[CalendarDayDatum]\n\n    @classmethod\n    def empty(cls) -> \"CalendarData\":\n        return CalendarData(days=[])\n"
  },
  {
    "path": "backend/src/models/wrapped/langs.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\n\n\nclass LangDatum(BaseModel):\n    id: str\n    label: str\n    value: int\n    formatted_value: str\n    color: str\n\n\nclass LangData(BaseModel):\n    langs_changed: List[LangDatum]\n    langs_added: List[LangDatum]\n\n    @classmethod\n    def empty(cls) -> \"LangData\":\n        return LangData(langs_changed=[], langs_added=[])\n"
  },
  {
    "path": "backend/src/models/wrapped/main.py",
    "content": "from pydantic import BaseModel\n\nfrom src.models.wrapped.calendar import CalendarData\nfrom src.models.wrapped.langs import LangData\nfrom src.models.wrapped.numeric import NumericData\nfrom src.models.wrapped.repos import RepoData\nfrom src.models.wrapped.time import DayData, MonthData\nfrom src.models.wrapped.timestamps import TimestampData\n\n\nclass WrappedPackage(BaseModel):\n    month_data: MonthData\n    day_data: DayData\n    calendar_data: CalendarData\n    numeric_data: NumericData\n    repo_data: RepoData\n    lang_data: LangData\n    timestamp_data: TimestampData\n    incomplete: bool = False\n\n    @classmethod\n    def empty(cls) -> \"WrappedPackage\":\n        return WrappedPackage(\n            month_data=MonthData.empty(),\n            day_data=DayData.empty(),\n            calendar_data=CalendarData.empty(),\n            numeric_data=NumericData.empty(),\n            repo_data=RepoData.empty(),\n            lang_data=LangData.empty(),\n            timestamp_data=TimestampData.empty(),\n        )\n"
  },
  {
    "path": "backend/src/models/wrapped/numeric.py",
    "content": "from typing import Optional, Tuple\n\nfrom pydantic import BaseModel\n\n\nclass ContribStats(BaseModel):\n    contribs: int\n    commits: int\n    issues: int\n    prs: int\n    reviews: int\n    other: int\n\n    @classmethod\n    def empty(cls) -> \"ContribStats\":\n        return ContribStats(\n            contribs=0,\n            commits=0,\n            issues=0,\n            prs=0,\n            reviews=0,\n            other=0,\n        )\n\n\nclass MiscStats(BaseModel):\n    total_days: int\n    longest_streak: int\n    longest_streak_days: Tuple[int, int, str, str]\n    longest_gap: int\n    longest_gap_days: Tuple[int, int, str, str]\n    weekend_percent: int\n    best_day_count: int\n    best_day_date: Optional[str]\n    best_day_index: Optional[int]\n\n    @classmethod\n    def empty(cls) -> \"MiscStats\":\n        return MiscStats(\n            total_days=0,\n            longest_streak=0,\n            longest_streak_days=(0, 0, \"\", \"\"),\n            longest_gap=0,\n            longest_gap_days=(0, 0, \"\", \"\"),\n            weekend_percent=0,\n            best_day_count=0,\n            best_day_date=None,\n            best_day_index=None,\n        )\n\n\nclass LOCStats(BaseModel):\n    loc_additions: str\n    loc_deletions: str\n    loc_changed: str\n    loc_added: str\n    loc_additions_per_commit: int\n    loc_deletions_per_commit: int\n    loc_changed_per_day: int\n\n    @classmethod\n    def empty(cls) -> \"LOCStats\":\n        return LOCStats(\n            loc_additions=\"0\",\n            loc_deletions=\"0\",\n            loc_changed=\"0\",\n            loc_added=\"0\",\n            loc_additions_per_commit=0,\n            loc_deletions_per_commit=0,\n            loc_changed_per_day=0,\n        )\n\n\nclass NumericData(BaseModel):\n    contribs: ContribStats\n    misc: MiscStats\n    loc: LOCStats\n\n    @classmethod\n    def empty(cls) -> \"NumericData\":\n        return NumericData(\n            contribs=ContribStats.empty(),\n            misc=MiscStats.empty(),\n            loc=LOCStats.empty(),\n        )\n"
  },
  {
    "path": "backend/src/models/wrapped/repos.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\n\n\nclass RepoDatum(BaseModel):\n    id: int\n    label: str\n    value: int\n    formatted_value: str\n\n\nclass RepoData(BaseModel):\n    repos_changed: List[RepoDatum]\n    repos_added: List[RepoDatum]\n\n    @classmethod\n    def empty(cls) -> \"RepoData\":\n        return RepoData(repos_changed=[], repos_added=[])\n"
  },
  {
    "path": "backend/src/models/wrapped/time.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\n\n\nclass TimeDatum(BaseModel):\n    index: int\n    contribs: int\n    loc_changed: int\n    formatted_loc_changed: str\n\n\nclass MonthData(BaseModel):\n    months: List[TimeDatum]\n\n    @classmethod\n    def empty(cls) -> \"MonthData\":\n        return MonthData(months=[])\n\n\nclass DayData(BaseModel):\n    days: List[TimeDatum]\n\n    @classmethod\n    def empty(cls) -> \"DayData\":\n        return DayData(days=[])\n"
  },
  {
    "path": "backend/src/models/wrapped/timestamps.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\n\n\nclass TimestampDatum(BaseModel):\n    type: str\n    weekday: int\n    timestamp: int\n\n\nclass TimestampData(BaseModel):\n    contribs: List[TimestampDatum]\n\n    @classmethod\n    def empty(cls) -> \"TimestampData\":\n        return TimestampData(contribs=[])\n"
  },
  {
    "path": "backend/src/processing/auth.py",
    "content": "from typing import Any, Dict, Optional, Tuple\n\nfrom src.data.github.auth import authenticate as github_authenticate\nfrom src.data.mongo.user import (\n    PublicUserModel,\n    delete_user as db_delete_user,\n    get_public_user as db_get_public_user,\n    update_user as db_update_user,\n)\nfrom src.models.background import UpdateUserBackgroundTask\n\n# frontend first calls set_user_key with code and user_key\n# next they call authenticate which determines the user_id to associate with the code/user_key\n# these actions should happen sequentially, so in-memory storage is fine\ncode_key_map: Dict[str, str] = {}\n\n\nasync def set_user_key(code: str, user_key: str) -> str:\n    code_key_map[code] = user_key\n    return user_key\n\n\nasync def authenticate(\n    code: str, private_access: bool\n) -> Tuple[str, Optional[UpdateUserBackgroundTask]]:\n    user_id, access_token = await github_authenticate(code)\n\n    curr_user: Optional[PublicUserModel] = await db_get_public_user(user_id)\n\n    raw_user: Dict[str, Any] = {\n        \"user_id\": user_id,\n        \"access_token\": access_token,\n        \"user_key\": code_key_map.get(code, None),\n        \"private_access\": private_access,\n    }\n\n    background_task = None\n    if curr_user is not None:\n        curr_private_access = curr_user.private_access\n        new_private_access = curr_private_access or private_access\n        raw_user[\"private_access\"] = new_private_access\n\n        if new_private_access != curr_private_access:\n            # new private access status\n            background_task = UpdateUserBackgroundTask(\n                user_id=user_id,\n                access_token=access_token,\n                private_access=new_private_access,\n                start_date=None,\n                end_date=None,\n            )\n    else:\n        # first time sign up\n        background_task = UpdateUserBackgroundTask(\n            user_id=user_id,\n            access_token=access_token,\n            private_access=private_access,\n            start_date=None,\n            end_date=None,\n        )\n\n    await db_update_user(user_id, raw_user)\n    return user_id, background_task\n\n\nasync def delete_user(user_id: str, user_key: str, use_user_key: bool = True) -> bool:\n    return await db_delete_user(user_id, user_key, use_user_key)\n"
  },
  {
    "path": "backend/src/processing/user/__init__.py",
    "content": "from src.processing.user.commits import get_top_languages, get_top_repos\nfrom src.processing.user.svg import svg_base\n\n__all__ = [\"get_top_languages\", \"get_top_repos\", \"svg_base\"]\n"
  },
  {
    "path": "backend/src/processing/user/commits.py",
    "content": "from typing import Any, Dict, List, Optional, Tuple, Union\n\nfrom src.constants import DEFAULT_COLOR\nfrom src.models import UserPackage\nfrom src.models.svg import LanguageStats, RepoStats\n\ndict_type = Dict[str, Union[str, int, float]]\n\n\ndef loc_metric_func(loc_metric: str, additions: int, deletions: int) -> int:\n    if loc_metric == \"changed\":\n        return additions + deletions\n    return additions - deletions\n\n\ndef get_top_languages(\n    data: UserPackage, loc_metric: str, include_private: bool\n) -> Tuple[List[LanguageStats], int]:\n    raw_languages = (\n        data.contribs.total_stats.languages\n        if include_private\n        else data.contribs.public_stats.languages\n    )\n\n    languages_list = [\n        LanguageStats(\n            lang=lang,\n            color=stats.color or DEFAULT_COLOR,\n            loc=loc_metric_func(loc_metric, stats.additions, stats.deletions),\n            percent=-1,\n        )\n        for lang, stats in raw_languages.items()\n    ]\n\n    languages_list = list(filter(lambda x: x.loc > 0, languages_list))\n\n    total_loc = sum(x.loc for x in languages_list) + 1\n    total = LanguageStats(lang=\"Total\", color=None, loc=total_loc, percent=100)\n\n    languages_list = sorted(languages_list, key=lambda x: x.loc, reverse=True)\n    other = LanguageStats(lang=\"Other\", color=\"#ededed\", loc=0, percent=-1)\n    for language in languages_list[4:]:\n        other.loc = other.loc + language.loc\n\n    languages_list = [total] + languages_list[:4] + [other]\n\n    new_languages_list: List[LanguageStats] = []\n    for lang in languages_list:\n        lang.percent = float(round(100 * lang.loc / total_loc, 2))\n        if lang.percent > 1:  # 1% minimum to show\n            new_languages_list.append(LanguageStats.model_validate(lang))\n\n    commits_excluded = data.contribs.public_stats.other_count\n    if include_private:\n        commits_excluded = data.contribs.total_stats.other_count\n\n    return new_languages_list, commits_excluded\n\n\ndef get_top_repos(\n    data: UserPackage, loc_metric: str, include_private: bool, group: str\n) -> Tuple[List[RepoStats], int]:\n    repos: List[Any] = [\n        {\n            \"repo\": repo,\n            \"private\": repo_stats.private,\n            \"langs\": [\n                {\n                    \"lang\": x[0],\n                    \"color\": x[1].color,\n                    \"loc\": loc_metric_func(loc_metric, x[1].additions, x[1].deletions),\n                }\n                for x in list(repo_stats.languages.items())\n            ],\n        }\n        for repo, repo_stats in data.contribs.repo_stats.items()\n        if include_private or not repo_stats.private\n    ]\n\n    for repo in repos:\n        repo[\"loc\"] = sum(x[\"loc\"] for x in repo[\"langs\"])  # first estimate\n    repos = list(filter(lambda x: x[\"loc\"] > 0, repos))\n\n    for repo in repos:\n        repo[\"langs\"] = [x for x in repo[\"langs\"] if x[\"loc\"] > 0.05 * repo[\"loc\"]]\n        repo[\"loc\"] = sum(x[\"loc\"] for x in repo[\"langs\"])  # final estimate\n    repos = sorted(repos, key=lambda x: x[\"loc\"], reverse=True)\n\n    new_repos = [\n        RepoStats.model_validate(x) for x in repos if x[\"loc\"] > 0.01 * repos[0][\"loc\"]\n    ]\n\n    commits_excluded = data.contribs.public_stats.other_count\n    if include_private:\n        commits_excluded = data.contribs.total_stats.other_count\n\n    # With n bars, group from n onwards into the last bar\n    bars = 4  # TODO: make this configurable (see issues)\n\n    if group == \"none\" or len(new_repos) <= bars:\n        return new_repos[:bars], commits_excluded\n\n    out_repos = []\n    other_repos = []\n    if group == \"other\":\n        out_repos = new_repos[: bars - 1]\n        other_repos = new_repos[bars - 1 :]\n    elif group == \"private\":\n        public_repos = [x for x in new_repos if not x.private]\n        private_repos = [x for x in new_repos if x.private]\n        if len(public_repos) < 4 and len(private_repos) > 0:\n            public_repos += private_repos[: bars - len(public_repos) - 1]\n            private_repos = private_repos[bars - len(public_repos) - 1 :]\n        out_repos = sorted(public_repos[: bars - 1], key=lambda x: x.loc, reverse=True)\n        other_repos = public_repos[bars - 1 :] + private_repos\n    else:\n        raise ValueError(\"Invalid group value\")\n\n    other: Dict[str, Tuple[int, Optional[str]]] = {}\n    for repo in other_repos:\n        for _lang in repo.langs:\n            lang = _lang.lang\n            if lang not in other:\n                other[lang] = (0, _lang.color)\n            other[lang] = (other[lang][0] + _lang.loc, other[lang][1])\n\n    out_repos.append(\n        RepoStats(\n            repo=\"other/repos\",\n            private=False,\n            langs=[{\"lang\": k, \"loc\": v[0], \"color\": v[1]} for k, v in other.items()],  # type: ignore\n            loc=sum(v[0] for v in other.values()),\n        )\n    )\n\n    return out_repos, commits_excluded\n"
  },
  {
    "path": "backend/src/processing/user/svg.py",
    "content": "from datetime import date\nfrom typing import Optional, Tuple\n\nfrom src.aggregation.layer2.user import get_user, get_user_demo\nfrom src.models import UserPackage\nfrom src.models.background import UpdateUserBackgroundTask\nfrom src.utils import use_time_range\n\n\nasync def svg_base(\n    user_id: str,\n    start_date: date,\n    end_date: date,\n    time_range: str,\n    demo: bool,\n    no_cache: bool = False,\n) -> Tuple[Optional[UserPackage], bool, Optional[UpdateUserBackgroundTask], str]:\n    # process time_range, start_date, end_date\n    time_range = \"one_month\" if demo else time_range\n    start_date, end_date, time_str = use_time_range(time_range, start_date, end_date)\n    complete = True  # overwritten later if not complete\n    background_task = None\n\n    # fetch data, either using demo or user method\n    if demo:\n        output = await get_user_demo(user_id, start_date, end_date, no_cache=no_cache)\n    else:\n        output, complete, background_task = await get_user(\n            user_id, start_date, end_date, no_cache=no_cache\n        )\n\n    return output, complete, background_task, time_str\n"
  },
  {
    "path": "backend/src/processing/wrapped/__init__.py",
    "content": "from src.processing.wrapped.main import query_wrapped_user\n\n__all__ = [\"query_wrapped_user\"]\n"
  },
  {
    "path": "backend/src/processing/wrapped/calendar.py",
    "content": "from datetime import datetime, timedelta\nfrom typing import Any, Dict, List\n\nfrom src.models import CalendarData, CalendarDayDatum, UserPackage\n\n\ndef get_calendar_data(data: UserPackage, year: int) -> CalendarData:\n    top_langs = [\n        x[0]\n        for x in sorted(\n            data.contribs.total_stats.languages.items(),\n            key=lambda x: x[1].additions + x[1].deletions,\n            reverse=True,\n        )[:5]\n    ]\n\n    total_out: List[CalendarDayDatum] = []\n    items_dict = {item.date: item for item in data.contribs.total}\n    for i in range(365):\n        date = (datetime(year, 1, 1) + timedelta(days=i - 1)).strftime(\"%Y-%m-%d\")\n        item = items_dict.get(date)\n        out: Dict[str, Any] = {\n            \"day\": date,\n            \"contribs\": 0,\n            \"commits\": 0,\n            \"issues\": 0,\n            \"prs\": 0,\n            \"reviews\": 0,\n            \"loc_added\": 0,\n            \"loc_changed\": 0,\n            \"top_langs\": {k: {\"loc_added\": 0, \"loc_changed\": 0} for k in top_langs},\n        }\n\n        if item is not None:\n            out[\"contribs\"] = item.stats.contribs_count\n            out[\"commits\"] = item.stats.commits_count\n            out[\"issues\"] = item.stats.issues_count\n            out[\"prs\"] = item.stats.prs_count\n            out[\"reviews\"] = item.stats.reviews_count\n\n            for k, v in item.stats.languages.items():\n                if k in top_langs:\n                    out[\"top_langs\"][k][\"loc_added\"] = v.additions - v.deletions\n                    out[\"top_langs\"][k][\"loc_changed\"] = v.additions + v.deletions\n                out[\"loc_added\"] += v.additions - v.deletions\n                out[\"loc_changed\"] += v.additions + v.deletions\n\n        out_obj = CalendarDayDatum.model_validate(out)\n        total_out.append(out_obj)\n\n    return CalendarData.model_validate({\"days\": total_out})\n"
  },
  {
    "path": "backend/src/processing/wrapped/langs.py",
    "content": "from typing import List\n\nfrom src.constants import DEFAULT_COLOR\nfrom src.models import LangData, LangDatum, Language, UserPackage\nfrom src.utils import format_number\n\n\ndef _count_loc(x: Language, metric: str) -> int:\n    if metric == \"changed\":\n        return x.additions + x.deletions\n    return x.additions - x.deletions\n\n\ndef get_lang_data(data: UserPackage) -> LangData:\n    out = {}\n    for m in [\"changed\", \"added\"]:\n        langs = sorted(\n            data.contribs.total_stats.languages.items(),\n            key=lambda x: _count_loc(x[1], m),\n            reverse=True,\n        )\n        lang_objs: List[LangDatum] = []\n        for k, v in list(langs)[:5]:\n            lang_data = {\n                \"id\": k,\n                \"label\": k,\n                \"value\": _count_loc(v, m),\n                \"formatted_value\": format_number(_count_loc(v, m)),\n                \"color\": v.color,\n            }\n            lang_objs.append(LangDatum.model_validate(lang_data))\n\n        # remaining languages\n        total_count = sum(_count_loc(v, m) for _, v in list(langs)[5:])\n        lang_data = {\n            \"id\": \"other\",\n            \"label\": \"other\",\n            \"value\": total_count,\n            \"formatted_value\": format_number(total_count),\n            \"color\": DEFAULT_COLOR,\n        }\n        if total_count > 100:\n            lang_objs.append(LangDatum.model_validate(lang_data))\n\n        out[f\"langs_{m}\"] = lang_objs\n\n    return LangData.model_validate(out)\n"
  },
  {
    "path": "backend/src/processing/wrapped/main.py",
    "content": "from datetime import date, timedelta\nfrom typing import Optional, Tuple\n\nfrom src.aggregation.layer1 import query_user\nfrom src.data.mongo.user import PublicUserModel, get_public_user as db_get_public_user\nfrom src.models import UserPackage, WrappedPackage\nfrom src.processing.wrapped.package import get_wrapped_data\nfrom src.utils import alru_cache\n\n\n@alru_cache(ttl=timedelta(hours=12))\nasync def query_wrapped_user(\n    user_id: str, year: int, no_cache: bool = False\n) -> Tuple[bool, Optional[WrappedPackage]]:\n    start_date, end_date = date(year, 1, 1), date(year, 12, 31)\n    user: Optional[PublicUserModel] = await db_get_public_user(user_id)\n    private_access = False\n    access_token = None\n    if user is not None and user.private_access:\n        private_access = True\n        access_token = user.access_token\n    user_package: UserPackage = await query_user(\n        user_id,\n        access_token,\n        private_access,\n        start_date,\n        end_date,\n        max_time=40,\n        no_cache=True,\n    )\n    wrapped_package = get_wrapped_data(user_package, year)\n\n    # Don't cache if incomplete\n    return (not wrapped_package.incomplete, wrapped_package)\n"
  },
  {
    "path": "backend/src/processing/wrapped/numeric.py",
    "content": "from collections import defaultdict\nfrom datetime import datetime\nfrom typing import Dict\n\nfrom src.models import ContribStats, LOCStats, MiscStats, NumericData, UserPackage\n\n\ndef get_contrib_stats(data: UserPackage) -> ContribStats:\n    return ContribStats.model_validate(\n        {\n            \"contribs\": data.contribs.total_stats.contribs_count,\n            \"commits\": data.contribs.total_stats.commits_count,\n            \"issues\": data.contribs.total_stats.issues_count,\n            \"prs\": data.contribs.total_stats.prs_count,\n            \"reviews\": data.contribs.total_stats.reviews_count,\n            \"other\": data.contribs.total_stats.other_count,\n        }\n    )\n\n\ndef get_misc_stats(data: UserPackage, year: int) -> MiscStats:\n    weekdays: Dict[int, int] = defaultdict(int)\n    yeardays, distinct_days, total_contribs = {}, 0, 0\n    for item in data.contribs.total:\n        count = item.stats.contribs_count\n        weekdays[item.weekday] += count\n        total_contribs += item.stats.contribs_count\n        if count > 0:\n            date = datetime.fromisoformat(item.date)\n            yeardays[date.timetuple().tm_yday - 1] = 1\n            distinct_days += 1\n    curr, best, best_dates = 0, 0, (1, 1)\n    for i in range(366):\n        curr = curr + 1 if i in yeardays else 0\n        if curr > best:\n            best = curr\n            best_dates = (i - curr + 2, i + 1)\n    longest_streak = max(best, curr)\n    longest_streak_days = (\n        best_dates[0],\n        best_dates[1],\n        datetime.fromordinal(max(1, best_dates[0])).strftime(\"%b %d\"),\n        datetime.fromordinal(max(1, best_dates[1])).strftime(\"%b %d\"),\n    )\n    curr, best, best_dates = 0, 0, (1, 1)\n    days = (datetime.now() - datetime(year, 1, 1)).days\n    for i in range(min(days, 365)):\n        curr = 0 if i in yeardays else curr + 1\n        if curr > best:\n            best = curr\n            best_dates = (i - curr + 2, i + 1)\n    longest_gap = max(best, curr)\n    longest_gap_days = (\n        best_dates[0],\n        best_dates[1],\n        datetime.fromordinal(max(1, best_dates[0])).strftime(\"%b %d\"),\n        datetime.fromordinal(max(1, best_dates[1])).strftime(\"%b %d\"),\n    )\n    weekend_percent = (weekdays[0] + weekdays[6]) / max(1, total_contribs)\n\n    best_day_count, best_day_date, best_day_index = 0, None, None\n    if len(data.contribs.total) > 0:\n        best_day = max(data.contribs.total, key=lambda x: x.stats.contribs_count)\n        best_day_index = datetime.fromisoformat(best_day.date).timetuple().tm_yday\n        best_day_count = best_day.stats.contribs_count\n        best_day_date = best_day.date\n\n    return MiscStats.model_validate(\n        {\n            \"total_days\": distinct_days,\n            \"longest_streak\": longest_streak,\n            \"longest_streak_days\": longest_streak_days,\n            \"longest_gap\": longest_gap,\n            \"longest_gap_days\": longest_gap_days,\n            \"weekend_percent\": round(100 * weekend_percent),\n            \"best_day_count\": best_day_count,\n            \"best_day_date\": best_day_date,\n            \"best_day_index\": best_day_index,\n        }\n    )\n\n\ndef format_loc_number(number: int) -> str:\n    if number < 1e3:\n        return str(100 * round(number / 100))\n    if number < 1e6:\n        return f\"{str(round(number / 1000.0))},000\"\n    return f\"{str(round(number / 1000000.0))},000,000\"\n\n\ndef get_loc_stats(data: UserPackage) -> LOCStats:\n    dataset = data.contribs.total_stats.languages.values()\n    return LOCStats.model_validate(\n        {\n            \"loc_additions\": format_loc_number(sum(x.additions for x in dataset)),\n            \"loc_deletions\": format_loc_number(sum(x.deletions for x in dataset)),\n            \"loc_changed\": format_loc_number(\n                sum(x.additions + x.deletions for x in dataset)\n            ),\n            \"loc_added\": format_loc_number(\n                sum(x.additions - x.deletions for x in dataset)\n            ),\n            \"loc_additions_per_commit\": round(\n                (\n                    sum(x.additions for x in dataset)\n                    / max(1, data.contribs.total_stats.commits_count)\n                )\n            ),\n            \"loc_deletions_per_commit\": round(\n                (\n                    sum(x.deletions for x in dataset)\n                    / max(1, data.contribs.total_stats.commits_count)\n                )\n            ),\n            \"loc_changed_per_day\": round(\n                sum(x.additions + x.deletions for x in dataset) / 365\n            ),\n        }\n    )\n\n\ndef get_numeric_data(data: UserPackage, year: int) -> NumericData:\n    return NumericData.model_validate(\n        {\n            \"contribs\": get_contrib_stats(data),\n            \"misc\": get_misc_stats(data, year),\n            \"loc\": get_loc_stats(data),\n        }\n    )\n"
  },
  {
    "path": "backend/src/processing/wrapped/package.py",
    "content": "from src.models import UserPackage, WrappedPackage\nfrom src.processing.wrapped.calendar import get_calendar_data\nfrom src.processing.wrapped.langs import get_lang_data\nfrom src.processing.wrapped.numeric import get_numeric_data\nfrom src.processing.wrapped.repos import get_repo_data\nfrom src.processing.wrapped.time import get_day_data, get_month_data\nfrom src.processing.wrapped.timestamps import get_timestamp_data\n\n# from src.processing.user.follows import get_user_follows\n\n\ndef get_wrapped_data(user_package: UserPackage, year: int) -> WrappedPackage:\n    \"\"\"packages all processing steps for the user query\"\"\"\n\n    month_data = get_month_data(user_package)\n    day_data = get_day_data(user_package)\n    calendar_data = get_calendar_data(user_package, year)\n    numeric_data = get_numeric_data(user_package, year)\n    repo_data = get_repo_data(user_package)\n    lang_data = get_lang_data(user_package)\n    timestamp_data = get_timestamp_data(user_package)\n\n    return WrappedPackage(\n        month_data=month_data,\n        day_data=day_data,\n        calendar_data=calendar_data,\n        numeric_data=numeric_data,\n        repo_data=repo_data,\n        lang_data=lang_data,\n        timestamp_data=timestamp_data,\n        incomplete=user_package.incomplete,\n    )\n"
  },
  {
    "path": "backend/src/processing/wrapped/repos.py",
    "content": "from typing import List\n\nfrom src.models import Language, RepoContributionStats, RepoData, RepoDatum, UserPackage\nfrom src.utils import format_number\n\n\ndef _count_loc(x: Language, metric: str) -> int:\n    if metric == \"changed\":\n        return x.additions + x.deletions\n    return x.additions - x.deletions\n\n\ndef _count_repo_loc(x: RepoContributionStats, metric: str) -> int:\n    return sum(_count_loc(lang, metric) for lang in x.languages.values())\n\n\ndef get_repo_data(data: UserPackage) -> RepoData:\n    out = {}\n    for m in [\"changed\", \"added\"]:\n        repos = sorted(\n            data.contribs.repo_stats.items(),\n            key=lambda x: _count_repo_loc(x[1], m),\n            reverse=True,\n        )\n        repo_objs: List[RepoDatum] = []\n\n        # first five repositories\n        for i, (k, v) in enumerate(list(repos)[:5]):\n            repo_data = {\n                \"id\": i,\n                \"label\": \"private/repository\" if v.private else k,\n                \"value\": _count_repo_loc(v, m),\n                \"formatted_value\": format_number(_count_repo_loc(v, m)),\n            }\n            repo_objs.append(RepoDatum.model_validate(repo_data))\n\n        # remaining repositories\n        total_count = sum(_count_repo_loc(v, m) for _, v in list(repos)[5:])\n        repo_data = {\n            \"id\": -1,\n            \"label\": \"other\",\n            \"value\": total_count,\n            \"formatted_value\": format_number(total_count),\n        }\n        if total_count > 100:\n            repo_objs.append(RepoDatum.model_validate(repo_data))\n\n        out[f\"repos_{m}\"] = repo_objs\n\n    return RepoData.model_validate(out)\n"
  },
  {
    "path": "backend/src/processing/wrapped/time.py",
    "content": "from collections import defaultdict\nfrom datetime import datetime\nfrom typing import Dict, List, Union\n\nfrom src.models import DayData, MonthData, TimeDatum, UserPackage\nfrom src.utils import format_number\n\n\ndef get_month_data(data: UserPackage) -> MonthData:\n    months: Dict[int, Dict[str, int]] = defaultdict(\n        lambda: {\"contribs\": 0, \"loc_changed\": 0}\n    )\n\n    for item in data.contribs.total:\n        month = datetime.fromisoformat(item.date).month - 1\n        months[month][\"contribs\"] += item.stats.contribs_count\n        loc_changed = sum(\n            x.additions + x.deletions for x in item.stats.languages.values()\n        )\n\n        months[month][\"loc_changed\"] += loc_changed\n\n    out: List[TimeDatum] = []\n    for k in range(12):\n        v = months[k]\n        _obj: Dict[str, Union[str, int]] = {\n            \"index\": k,\n            **v,\n            \"formatted_loc_changed\": format_number(v[\"loc_changed\"]),\n        }\n\n        out.append(TimeDatum.model_validate(_obj))\n\n    return MonthData(months=out)\n\n\ndef get_day_data(data: UserPackage) -> DayData:\n    days: Dict[int, Dict[str, int]] = defaultdict(\n        lambda: {\"contribs\": 0, \"loc_changed\": 0}\n    )\n\n    for item in data.contribs.total:\n        day = (datetime.fromisoformat(item.date).weekday() + 1) % 7\n        days[day][\"contribs\"] += item.stats.contribs_count\n        loc_changed = sum(\n            x.additions + x.deletions for x in item.stats.languages.values()\n        )\n\n        days[day][\"loc_changed\"] += loc_changed\n\n    out: List[TimeDatum] = []\n    for k in range(7):\n        v = days[k]\n        _obj: Dict[str, Union[str, int]] = {\n            \"index\": k,\n            **v,\n            \"formatted_loc_changed\": format_number(v[\"loc_changed\"]),\n        }\n\n        out.append(TimeDatum.model_validate(_obj))\n\n    return DayData(days=out)\n"
  },
  {
    "path": "backend/src/processing/wrapped/timestamps.py",
    "content": "from datetime import datetime\nfrom random import shuffle\nfrom typing import Any, List\n\nfrom src.models import TimestampData, TimestampDatum, UserPackage\n\nMAX_ITEMS = 200\n\n\ndef date_to_seconds_since_midnight(date: datetime) -> int:\n    return (date.hour * 60 * 60) + (date.minute * 60) + date.second\n\n\ndef get_timestamp_data(data: UserPackage) -> TimestampData:\n    out: List[Any] = []\n    for item in data.contribs.total:\n        lists = item.lists\n        lists = [lists.commits, lists.issues, lists.prs, lists.reviews]\n        for type, list in zip([\"commit\", \"issue\", \"pr\", \"review\"], lists):\n            out.extend(\n                {\n                    \"type\": type,\n                    \"weekday\": item.weekday,\n                    \"timestamp\": date_to_seconds_since_midnight(obj),\n                }\n                for obj in list\n            )\n\n    shuffle(out)\n    out = out[:MAX_ITEMS]\n    out = [TimestampDatum.model_validate(x) for x in out]\n\n    return TimestampData(contribs=out)\n"
  },
  {
    "path": "backend/src/render/__init__.py",
    "content": "from src.render.error import (\n    get_empty_demo_svg,\n    get_error_svg,\n    get_loading_svg,\n    get_no_data_svg,\n)\nfrom src.render.top_langs import get_top_langs_svg\nfrom src.render.top_repos import get_top_repos_svg\n\n__all__ = [\n    \"get_empty_demo_svg\",\n    \"get_error_svg\",\n    \"get_loading_svg\",\n    \"get_no_data_svg\",\n    \"get_top_langs_svg\",\n    \"get_top_repos_svg\",\n]\n"
  },
  {
    "path": "backend/src/render/error.py",
    "content": "# type: ignore\n\nfrom svgwrite import Drawing\n\nfrom src.constants import BACKEND_URL\nfrom src.render.style import styles_no_animation, themes\nfrom src.render.template import get_template\n\nTHEME = \"classic\"\n\n\ndef get_error_svg() -> Drawing:\n    d = Drawing(size=(300, 285))\n    d.defs.add(d.style(styles_no_animation[THEME]))\n\n    d.add(\n        d.rect(\n            size=(299, 284),\n            insert=(0.5, 0.5),\n            rx=4.5,\n            stroke=themes[THEME][\"border_color\"],\n            fill=themes[THEME][\"bg_color\"],\n        )\n    )\n\n    d.add(d.text(\"Unknown Error\", insert=(25, 35), class_=f\"{THEME}-header\"))\n\n    d.add(\n        d.text(\n            \"Please try again later or raise a ticket on GitHub\",\n            insert=(25, 60),\n            class_=f\"{THEME}-lang-name\",\n        )\n    )\n\n    d.add(\n        d.image(f\"{BACKEND_URL}/assets/error\", insert=(85, 100), style=\"opacity: 50%\")\n    )\n\n    return d\n\n\ndef get_empty_demo_svg(header: str) -> Drawing:\n    d = Drawing(size=(300, 285))\n    d.defs.add(d.style(styles_no_animation[THEME]))\n\n    d.add(\n        d.rect(\n            size=(299, 284),\n            insert=(0.5, 0.5),\n            rx=4.5,\n            stroke=themes[THEME][\"border_color\"],\n            fill=themes[THEME][\"bg_color\"],\n        )\n    )\n\n    d.add(d.text(header, insert=(25, 35), class_=f\"{THEME}-header\"))\n\n    d.add(\n        d.text(\n            \"Enter your username to start!\",\n            insert=(25, 60),\n            class_=f\"{THEME}-lang-name\",\n        )\n    )\n\n    d.add(\n        d.image(\n            f\"{BACKEND_URL}/assets/stopwatch\", insert=(85, 100), style=\"opacity: 50%\"\n        )\n    )\n\n    return d\n\n\ndef get_loading_svg() -> Drawing:\n    d = Drawing(size=(300, 285))\n    d.defs.add(d.style(styles_no_animation[THEME]))\n\n    d.add(\n        d.rect(\n            size=(299, 284),\n            insert=(0.5, 0.5),\n            rx=4.5,\n            stroke=themes[THEME][\"border_color\"],\n            fill=themes[THEME][\"bg_color\"],\n        )\n    )\n\n    d.add(\n        d.text(\"Loading data, hang tight!\", insert=(25, 35), class_=f\"{THEME}-header\")\n    )\n\n    d.add(\n        d.text(\n            \"Please wait a couple seconds and refresh the page.\",\n            insert=(25, 60),\n            class_=f\"{THEME}-lang-name\",\n        )\n    )\n\n    d.add(\n        d.image(\n            f\"{BACKEND_URL}/assets/stopwatch\", insert=(85, 100), style=\"opacity: 50%\"\n        )\n    )\n\n    return d\n\n\ndef get_no_data_svg(header: str, subheader: str) -> Drawing:\n    d, dp = get_template(\n        width=300,\n        height=285,\n        padding=20,\n        header_text=header,\n        subheader_text=subheader,\n        debug=False,\n        theme=THEME,\n    )\n\n    d.add(d.image(f\"{BACKEND_URL}/assets/error\", insert=(85, 80), style=\"opacity: 50%\"))\n\n    dp.add(d.text(\"No data to show\", insert=(55, 220), class_=f\"{THEME}-image-text\"))\n\n    d.add(dp)\n    return d\n"
  },
  {
    "path": "backend/src/render/style.py",
    "content": "from typing import List, Tuple\n\nthemes = {\n    \"classic\": {\n        \"header_color\": \"#2f80ed\",\n        \"subheader_color\": \"#666\",\n        \"text_color\": \"#333\",\n        \"bg_color\": \"#fffefe\",\n        \"border_color\": \"#e4e2e2\",\n        \"bar_color\": \"#ddd\",\n    },\n    \"dark\": {\n        \"header_color\": \"#fff\",\n        \"subheader_color\": \"#9f9f9f\",\n        \"text_color\": \"#9f9f9f\",\n        \"bg_color\": \"#151515\",\n        \"border_color\": \"#e4e2e2\",\n        \"bar_color\": \"#333\",\n    },\n    \"bright_lights\": {\n        \"header_color\": \"#fff\",\n        \"subheader_color\": \"#0e86d4\",\n        \"text_color\": \"#b1d4e0\",\n        \"bg_color\": \"#003060\",\n        \"border_color\": \"#0e86d4\",\n        \"bar_color\": \"#ddd\",\n    },\n    \"rosettes\": {\n        \"header_color\": \"#fff\",\n        \"subheader_color\": \"#b6e2d3\",\n        \"text_color\": \"#fae8e0\",\n        \"bg_color\": \"#ef7c8e\",\n        \"border_color\": \"#b6e2d3\",\n        \"bar_color\": \"#ddd\",\n    },\n    \"ferns\": {\n        \"header_color\": \"#116530\",\n        \"subheader_color\": \"#18a558\",\n        \"text_color\": \"#116530\",\n        \"bg_color\": \"#a3ebb1\",\n        \"border_color\": \"#21b6a8\",\n        \"bar_color\": \"#ddd\",\n    },\n    \"synthwaves\": {\n        \"header_color\": \"#e2e9ec\",\n        \"subheader_color\": \"#e5289e\",\n        \"text_color\": \"#ef8539\",\n        \"bg_color\": \"#2b213a\",\n        \"border_color\": \"#e5289e\",\n        \"bar_color\": \"#ddd\",\n    },\n}\n\n\ndef get_style(theme: str = \"classic\", use_animation: bool = True) -> str:\n    # List[Tuple[\"selector\", List[Tuple[\"property\", \"is_animation\"]], \"is_animation\"]]\n    _style: List[Tuple[str, List[Tuple[str, bool]], bool]] = [\n        (\n            \".header\",\n            [\n                (\"font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;\", False),\n                (\"fill: \" + themes[theme][\"header_color\"] + \";\", False),\n                (\"animation: fadeInAnimation 0.8s ease-in-out forwards;\", True),\n            ],\n            False,\n        ),\n        (\n            \".subheader\",\n            [\n                (\"font: 500 10px 'Segoe UI', Ubuntu, Sans-Serif;\", False),\n                (\"fill: \" + themes[theme][\"subheader_color\"] + \";\", False),\n                (\"animation: fadeInAnimation 0.8s ease-in-out forwards;\", True),\n            ],\n            False,\n        ),\n        (\n            \".lang-name\",\n            [\n                (\"font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif;\", False),\n                (\"fill: \" + themes[theme][\"text_color\"] + \";\", False),\n            ],\n            False,\n        ),\n        (\n            \".image-text\",\n            [\n                (\"font: 500 20px 'Segoe UI', Ubuntu, Sans-Serif;\", False),\n                (\"fill: \" + themes[theme][\"text_color\"] + \";\", False),\n                (\"opacity: 50%;\", False),\n            ],\n            False,\n        ),\n        (\n            \"@keyframes fadeInAnimation\",\n            [(\"from { opacity: 0; } to { opacity: 1; }\", True)],\n            True,\n        ),\n    ]\n\n    return \"\\n\".join(\n        [\n            (\n                (rule[0].replace(\".\", f\".{theme}-\") + \" {\")\n                + \"\\n\".join(\n                    item[0] for item in rule[1] if (use_animation or not item[1])\n                )\n                + \"}\"\n            )\n            for rule in _style\n            if use_animation or not rule[2]\n        ]\n    )\n\n\nstyles = {k: get_style(k) for k in themes.keys()}\nstyles_no_animation = {k: get_style(k, False) for k in themes.keys()}\n"
  },
  {
    "path": "backend/src/render/template.py",
    "content": "# type: ignore\n\nfrom typing import List, Tuple\n\nfrom svgwrite import Drawing\nfrom svgwrite.container import Group\nfrom svgwrite.shapes import Circle\n\nfrom src.constants import DEFAULT_COLOR\nfrom src.render.style import styles, styles_no_animation, themes\n\n\ndef get_template(\n    width: int,\n    height: int,\n    padding: int,\n    header_text: str,\n    subheader_text: str,\n    theme: str,\n    use_animation: bool = True,\n    debug: bool = False,\n) -> Tuple[Drawing, Group]:\n    d = Drawing(size=(width, height))\n\n    style = styles[theme]\n    style_no_animation = styles_no_animation[theme]\n    d.defs.add(d.style(style if use_animation else style_no_animation))\n\n    d.add(\n        d.rect(\n            size=(width - 1, height - 1),\n            insert=(0.5, 0.5),\n            rx=4.5,\n            stroke=themes[theme][\"border_color\"],\n            fill=themes[theme][\"bg_color\"],\n        )\n    )\n\n    d.add(\n        d.rect(\n            size=(width - 2 * padding, height - 2 * padding),\n            insert=(padding, padding),\n            fill=\"#eee\" if debug else themes[theme][\"bg_color\"],\n        )\n    )\n\n    dp = Group(transform=f\"translate({padding}, {padding})\")\n\n    dp.add(d.text(header_text, insert=(0, 13), class_=f\"{theme}-header\"))\n    dp.add(d.text(subheader_text, insert=(0, 31), class_=f\"{theme}-subheader\"))\n\n    return d, dp\n\n\ndef get_bar_section(\n    d: Drawing,\n    dataset: List[Tuple[str, str, List[Tuple[float, str]]]],\n    theme: str,\n    padding: int = 45,\n    bar_width: int = 210,\n) -> Group:\n    section = Group(transform=f\"translate(0, {padding})\")\n    for i, (top_text, right_text, data_row) in enumerate(dataset):\n        translate = f\"translate(0, {str(40 * i)})\"\n        row = Group(transform=translate)\n        row.add(d.text(top_text, insert=(2, 15), class_=f\"{theme}-lang-name\"))\n        row.add(\n            d.text(right_text, insert=(bar_width + 10, 33), class_=f\"{theme}-lang-name\")\n        )\n\n        progress = Drawing(width=str(bar_width), x=\"0\", y=\"25\")\n        progress.add(\n            d.rect(\n                size=(bar_width, 8),\n                insert=(0, 0),\n                rx=5,\n                ry=5,\n                fill=themes[theme][\"bar_color\"],\n            )\n        )\n        total_percent, total_items = 0, len(data_row)\n        diff = max(0, 300 / bar_width - data_row[-1][0])\n        for j, (percent, color) in enumerate(data_row):\n            color = color or DEFAULT_COLOR\n            if j == 0:\n                percent -= diff\n            elif j == total_items - 1:\n                percent += diff\n            bar_percent = bar_width * percent / 100\n            bar_total = bar_width * total_percent / 100\n            box_size, insert = (bar_percent, 8), (bar_total, 0)\n            progress.add(d.rect(size=box_size, insert=insert, rx=5, ry=5, fill=color))\n\n            width = min(bar_percent / 2, 5)\n            if total_items > 1:\n                box_left, box_right = j > 0, j < total_items - 1\n                box_size, insert = bar_percent - 2 * width, bar_total + width\n                if box_left:\n                    box_size += width\n                    insert -= width\n                if box_right:\n                    box_size += width\n                progress.add(d.rect(size=(box_size, 8), insert=(insert, 0), fill=color))\n\n            total_percent += percent\n        row.add(progress)\n        section.add(row)\n    return section\n\n\ndef get_lang_name_section(\n    d: Drawing,\n    data: List[Tuple[str, str]],\n    theme: str,\n    columns: int = 2,\n    padding: int = 80,\n) -> Group:\n    section = Group(transform=f\"translate(0, {padding})\")\n    for i, x in enumerate(data):\n        x_translate = str((260 / columns) * (i % columns))\n        y_translate = str(20 * (i // columns))\n        lang = Group(transform=f\"translate({x_translate}, {y_translate})\")\n        lang.add(Circle(center=(5, 5), r=5, fill=(data[i][1] or DEFAULT_COLOR)))\n        lang.add(d.text(data[i][0], insert=(14, 9), class_=f\"{theme}-lang-name\"))\n        section.add(lang)\n    return section\n"
  },
  {
    "path": "backend/src/render/top_langs.py",
    "content": "# type: ignore\n\nfrom typing import List, Tuple\n\nfrom svgwrite import Drawing\n\nfrom src.models.svg import LanguageStats\nfrom src.render.error import get_no_data_svg\nfrom src.render.template import get_bar_section, get_lang_name_section, get_template\nfrom src.utils import format_number\n\n\ndef get_top_langs_svg(\n    data: List[LanguageStats],\n    time_str: str,\n    use_percent: bool,\n    loc_metric: str,\n    complete: bool,\n    commits_excluded: int,\n    compact: bool,\n    use_animation: bool,\n    theme: str,\n) -> Drawing:\n    header = \"Most Used Languages\"\n    subheader = time_str\n    if not use_percent:\n        subheader += \" | \" + (\"LOC Changed\" if loc_metric == \"changed\" else \"LOC Added\")\n    if not complete:\n        subheader += \" | Incomplete (refresh to update)\"\n    elif commits_excluded > 50:\n        subheader += f\" | {commits_excluded} commits excluded\"\n\n    if len(data) <= 1:\n        return get_no_data_svg(header, subheader)\n\n    d, dp = get_template(\n        width=300,\n        height=175 if compact else 285,\n        padding=20,\n        header_text=header,\n        subheader_text=subheader,\n        use_animation=use_animation,\n        debug=False,\n        theme=theme,\n    )\n\n    dataset: List[Tuple[str, str, List[Tuple[float, str]]]] = []\n    padding, width = 0, 0\n    if compact:\n        data_row = [(x.percent, x.color) for x in data[1:6]]\n        dataset.append((\"\", \"\", data_row))\n        padding, width = 30, 260\n    else:\n        max_length = max(data[i].loc for i in range(1, len(data)))\n        for x in data[1:6]:\n            if use_percent:\n                dataset.append((x.lang, f\"{str(x.percent)}%\", [(x.percent, x.color)]))\n            else:\n                percent = 100 * x.loc / max_length\n                dataset.append((x.lang, format_number(x.loc), [(percent, x.color)]))\n        padding, width = 45, 210 if use_percent else 195\n\n    dp.add(\n        get_bar_section(\n            d=d, dataset=dataset, theme=theme, padding=padding, bar_width=width\n        )\n    )\n\n    langs = [(f\"{x.lang} {str(x.percent)}%\", x.color) for x in data[1:6]]\n    if compact:\n        dp.add(get_lang_name_section(d=d, data=langs, theme=theme))\n\n    d.add(dp)\n    return d\n"
  },
  {
    "path": "backend/src/render/top_repos.py",
    "content": "# type: ignore\n\nfrom collections import defaultdict\nfrom typing import List, Tuple\n\nfrom svgwrite import Drawing\n\nfrom src.models.svg import RepoStats\nfrom src.render.error import get_no_data_svg\nfrom src.render.template import get_bar_section, get_lang_name_section, get_template\nfrom src.utils import format_number\n\n\ndef get_top_repos_svg(\n    data: List[RepoStats],\n    time_str: str,\n    loc_metric: str,\n    complete: bool,\n    commits_excluded: int,\n    use_animation: bool,\n    theme: str,\n) -> Drawing:\n    header = \"Most Contributed Repositories\"\n    subheader = time_str\n    subheader += \" | \" + (\"LOC Changed\" if loc_metric == \"changed\" else \"LOC Added\")\n    if not complete:\n        subheader += \" | Incomplete (refresh to update)\"\n    elif commits_excluded > 50:\n        subheader += f\" | {commits_excluded} commits excluded\"\n\n    if len(data) == 0:\n        return get_no_data_svg(header, subheader)\n\n    d, dp = get_template(\n        width=300,\n        height=285,\n        padding=20,\n        header_text=header,\n        subheader_text=subheader,\n        use_animation=use_animation,\n        debug=False,\n        theme=theme,\n    )\n\n    dataset: List[Tuple[str, str, List[Tuple[float, str]]]] = []\n    total = max(x.loc for x in data)\n    for x in data[:4]:\n        data_row = [\n            (100 * lang.loc / total, lang.color)\n            for lang in sorted(x.langs, key=lambda x: x.loc, reverse=True)\n        ]\n\n        name = \"private/repository\" if x.private else x.repo\n        dataset.append((name, format_number(x.loc), data_row))\n\n    dp.add(\n        get_bar_section(d=d, dataset=dataset, theme=theme, padding=45, bar_width=195)\n    )\n\n    langs = defaultdict(int)\n    for x in data[:4]:\n        for lang in x.langs:\n            langs[(lang.lang, lang.color)] += lang.loc\n    langs = sorted(langs.items(), key=lambda x: x[1], reverse=True)\n    langs = [lang[0] for lang in langs[:6]]\n\n    columns = {1: 1, 2: 2, 3: 3, 4: 2, 5: 3, 6: 3}[len(langs)]\n    padding = 215 + (10 if columns == len(langs) else 0)\n    dp.add(\n        get_lang_name_section(\n            d=d, data=langs, theme=theme, columns=columns, padding=padding\n        )\n    )\n\n    d.add(dp)\n    return d\n"
  },
  {
    "path": "backend/src/routers/__init__.py",
    "content": "from src.routers.assets.assets import router as asset_router\nfrom src.routers.auth.main import router as auth_router\nfrom src.routers.dev import router as dev_router\nfrom src.routers.users.main import router as user_router\nfrom src.routers.wrapped import router as wrapped_router\n\n__all__ = [\"asset_router\", \"auth_router\", \"dev_router\", \"user_router\", \"wrapped_router\"]\n"
  },
  {
    "path": "backend/src/routers/assets/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/routers/assets/assets.py",
    "content": "from fastapi import APIRouter, status\nfrom fastapi.responses import FileResponse\n\nrouter = APIRouter()\n\n\n@router.get(\"/error\", status_code=status.HTTP_200_OK, include_in_schema=False)\nasync def get_error_img():\n    return FileResponse(\"./src/routers/assets/assets/error.png\")\n\n\n@router.get(\"/stopwatch\", status_code=status.HTTP_200_OK, include_in_schema=False)\nasync def get_stopwatch_img():\n    return FileResponse(\"./src/routers/assets/assets/stopwatch.png\")\n"
  },
  {
    "path": "backend/src/routers/auth/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/routers/auth/main.py",
    "content": "from fastapi import APIRouter\n\nfrom src.routers.auth.standalone import router as standalone_router\nfrom src.routers.auth.website import router as website_router\n\nrouter = APIRouter()\nrouter.include_router(standalone_router, prefix=\"\")\nrouter.include_router(website_router, prefix=\"/web\")\n"
  },
  {
    "path": "backend/src/routers/auth/standalone.py",
    "content": "import logging\nfrom typing import Optional\n\nfrom fastapi import APIRouter\nfrom fastapi.responses import RedirectResponse\n\nfrom src.constants import OAUTH_CLIENT_ID\nfrom src.processing.auth import authenticate, delete_user\nfrom src.routers.decorators import get_redirect_url\n\nrouter = APIRouter()\n\n\n@router.get(\"/signup/public\")\ndef redirect_public(user_id: Optional[str] = None) -> RedirectResponse:\n    return RedirectResponse(get_redirect_url(private=False, user_id=user_id))\n\n\n@router.get(\"/signup/private\")\ndef redirect_private(user_id: Optional[str] = None) -> RedirectResponse:\n    return RedirectResponse(get_redirect_url(private=True, user_id=user_id))\n\n\n@router.get(\"/redirect\", include_in_schema=False)\nasync def redirect_return(code: str = \"\", private_access: bool = False) -> str:\n    try:\n        user_id = await authenticate(code=code, private_access=private_access)\n        return f\"You ({user_id}) are now authenticated!\"\n    except Exception as e:\n        logging.exception(e)\n        return \"Unknown Error. Please try again later.\"\n\n\n@router.get(\"/delete/{user_id}\")\nasync def delete_account_auth(user_id: str) -> RedirectResponse:\n    return RedirectResponse(\n        get_redirect_url(prefix=f\"delete/{user_id}\", private=False, user_id=user_id)\n    )\n\n\n@router.get(\"/redirect/delete/{user_id}\", include_in_schema=False)\nasync def delete_account(user_id: str) -> RedirectResponse:\n    await delete_user(user_id, user_key=\"\", use_user_key=False)\n    return RedirectResponse(\n        f\"https://github.com/settings/connections/applications/{OAUTH_CLIENT_ID}\"\n    )\n"
  },
  {
    "path": "backend/src/routers/auth/website.py",
    "content": "from typing import Any, Dict\n\nfrom fastapi import BackgroundTasks, status\nfrom fastapi.responses import Response\nfrom fastapi.routing import APIRouter\n\nfrom src.processing.auth import authenticate, delete_user, set_user_key\nfrom src.routers.background import run_in_background\nfrom src.utils import async_fail_gracefully\n\nrouter = APIRouter()\n\n\n@router.post(\n    \"/set_user_key/{code}/{user_key}\",\n    status_code=status.HTTP_200_OK,\n    include_in_schema=False,\n    response_model=Dict[str, Any],\n)\n@async_fail_gracefully\nasync def set_user_key_endpoint(response: Response, code: str, user_key: str) -> str:\n    return await set_user_key(code, user_key)\n\n\n@router.post(\n    \"/login/{code}\",\n    status_code=status.HTTP_200_OK,\n    include_in_schema=False,\n    response_model=Dict[str, Any],\n)\n@async_fail_gracefully\nasync def authenticate_endpoint(\n    response: Response,\n    background_tasks: BackgroundTasks,\n    code: str,\n    private_access: bool = False,\n) -> str:\n    output, background_task = await authenticate(code, private_access)\n    if background_task is not None:\n        # set a background task to update the user\n        background_tasks.add_task(run_in_background, task=background_task)\n    return output\n\n\n@router.get(\n    \"/delete/{user_id}\",\n    status_code=status.HTTP_200_OK,\n    include_in_schema=False,\n    response_model=Dict[str, Any],\n)\n@async_fail_gracefully\nasync def delete_user_endpoint(response: Response, user_id: str, user_key: str) -> bool:\n    return await delete_user(user_id, user_key=user_key)\n"
  },
  {
    "path": "backend/src/routers/background.py",
    "content": "from typing import Dict\n\nfrom src.aggregation.layer1 import query_user\nfrom src.models.background import UpdateUserBackgroundTask\n\n# create a cache for the function\ncache: Dict[str, Dict[str, bool]] = {\"update_user\": {}}\n\n\nasync def run_in_background(task: UpdateUserBackgroundTask):\n    if isinstance(task, UpdateUserBackgroundTask):  # type: ignore\n        inputs = {\n            \"user_id\": task.user_id,\n            \"access_token\": task.access_token,\n            \"private_access\": task.private_access,\n            \"start_date\": task.start_date,\n            \"end_date\": task.end_date,\n        }\n\n        inputs = {k: v for k, v in inputs.items() if v is not None}\n\n        # check if the task is already running\n        if task.user_id in cache[\"update_user\"]:\n            return\n\n        # add the task to the cache\n        cache[\"update_user\"][task.user_id] = True\n\n        await query_user(**inputs)  # type: ignore\n\n        # remove the task from the cache\n        del cache[\"update_user\"][task.user_id]\n"
  },
  {
    "path": "backend/src/routers/decorators.py",
    "content": "import io\nimport logging\nfrom datetime import datetime\nfrom functools import wraps\nfrom typing import Any, Callable, Dict, List, Optional\n\nfrom fastapi import Response, status\nfrom starlette.responses import RedirectResponse\nfrom svgwrite.drawing import Drawing  # type: ignore\n\nfrom src.constants import OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI\nfrom src.render import get_error_svg\n\n\n# for standalone auth routes\ndef get_redirect_url(\n    prefix: str = \"\", private: bool = False, user_id: Optional[str] = None\n) -> str:\n    url = (\n        \"https://github.com/login/oauth/authorize?client_id=\"\n        + OAUTH_CLIENT_ID\n        + \"&redirect_uri=\"\n        + OAUTH_REDIRECT_URI\n        + \"/redirect\"\n    )\n\n    # add prefix to redirect to different backend routes\n    if prefix != \"\":\n        url += f\"/{prefix}\"\n\n    # add private flag to request correct permissions\n    if private:\n        url += \"?private_access=True&scope=user,repo\"\n    else:\n        url += \"?private_access=False\"\n\n    # add user_id to hint if provided\n    if user_id is not None:\n        url += f\"&login={user_id}\"\n\n    return url\n\n\n# NOTE: implied async, sync not implemented yet\ndef svg_fail_gracefully(func: Callable[..., Any]):\n    @wraps(func)  # needed to play nice with FastAPI decorator\n    async def wrapper(\n        response: Response, *args: List[Any], **kwargs: Dict[str, Any]\n    ) -> Any:\n        d: Drawing\n        start = datetime.now()\n        cache_max_age = 3600\n        try:\n            d = await func(response, *args, **kwargs)\n        except LookupError as e:\n            if \"user_id\" in kwargs:\n                user_id: str = kwargs[\"user_id\"]  # type: ignore\n                url = get_redirect_url(private=False, user_id=user_id)\n                return RedirectResponse(url)\n            logging.exception(e)\n            d = get_error_svg()\n            cache_max_age = 0\n        except Exception as e:\n            logging.exception(e)\n            d = get_error_svg()\n            cache_max_age = 0\n\n        sio = io.StringIO()\n        d.write(sio)  # type: ignore\n\n        print(\"SVG\", datetime.now() - start)\n\n        return Response(\n            sio.getvalue(),\n            media_type=\"image/svg+xml\",\n            status_code=status.HTTP_200_OK,\n            headers={\"Cache-Control\": f\"public, max-age={cache_max_age}\"},\n        )\n\n    return wrapper\n"
  },
  {
    "path": "backend/src/routers/dev.py",
    "content": "from datetime import date, timedelta\nfrom typing import Any, Dict, Optional\n\nfrom fastapi import APIRouter, Response, status\n\nfrom src.aggregation.layer0 import get_user_data\nfrom src.data.mongo.secret import update_keys\nfrom src.models import UserPackage, WrappedPackage\nfrom src.processing.wrapped.package import get_wrapped_data\nfrom src.utils import async_fail_gracefully, use_time_range\n\nrouter = APIRouter()\n\n\n@router.get(\n    \"/user/{user_id}\", status_code=status.HTTP_200_OK, response_model=Dict[str, Any]\n)\n@async_fail_gracefully\nasync def get_user_raw(\n    response: Response,\n    user_id: str,\n    access_token: Optional[str] = None,\n    start_date: date = date.today() - timedelta(365),\n    end_date: date = date.today(),\n    time_range: str = \"one_month\",\n    timezone_str: str = \"US/Eastern\",\n    full: bool = False,\n) -> UserPackage:\n    await update_keys()\n    start_date, end_date, _ = use_time_range(time_range, start_date, end_date)\n    return await get_user_data(\n        user_id, start_date, end_date, timezone_str, access_token\n    )\n\n\n@router.get(\n    \"/wrapped/{user_id}\", status_code=status.HTTP_200_OK, response_model=Dict[str, Any]\n)\n@async_fail_gracefully\nasync def get_wrapped_user_raw(\n    response: Response,\n    user_id: str,\n    year: int = 2024,\n    access_token: Optional[str] = None,\n) -> WrappedPackage:\n    await update_keys()\n    user_data = await get_user_data(\n        user_id, date(year, 1, 1), date(year, 12, 31), \"US/Eastern\", access_token\n    )\n    return get_wrapped_data(user_data, year)\n"
  },
  {
    "path": "backend/src/routers/users/__init__.py",
    "content": ""
  },
  {
    "path": "backend/src/routers/users/db.py",
    "content": "from typing import Any, Dict, Optional\n\nfrom fastapi import APIRouter, Response, status\n\nfrom src.data.mongo.secret import update_keys\nfrom src.data.mongo.user import PublicUserModel, get_public_user as db_get_public_user\nfrom src.utils import async_fail_gracefully\n\nrouter = APIRouter()\n\n\n@router.get(\n    \"/update_keys\",\n    status_code=status.HTTP_200_OK,\n    include_in_schema=False,\n    response_model=Dict[str, Any],\n)\n@async_fail_gracefully\nasync def update_keys_endpoint(response: Response) -> bool:\n    await update_keys(no_cache=True)\n    return True\n\n\n@router.get(\n    \"/get/metadata/{user_id}\",\n    status_code=status.HTTP_200_OK,\n    include_in_schema=False,\n    response_model=Dict[str, Any],\n)\n@async_fail_gracefully\nasync def get_db_public_user(\n    response: Response, user_id: str, no_cache: bool = False\n) -> Optional[PublicUserModel]:\n    return await db_get_public_user(user_id, no_cache=no_cache)\n\n\n\"\"\"\n@router.get(\"/get/{user_id}\", status_code=status.HTTP_200_OK, include_in_schema=False)\n@async_fail_gracefully\nasync def get_db_user(\n    response: Response, user_id: str, no_cache: bool = False\n) -> Optional[ExternalUserModel]:\n    user: Optional[UserModel] = await get_user_by_user_id(user_id, no_cache=no_cache)\n    if user is None:\n        return None\n    return ExternalUserModel.parse_obj(user.dict())\n\"\"\"\n"
  },
  {
    "path": "backend/src/routers/users/main.py",
    "content": "from datetime import date, timedelta\nfrom typing import Any, Dict, Optional\n\nfrom fastapi import APIRouter, BackgroundTasks, Response, status\n\nfrom src.aggregation.layer2 import get_user\nfrom src.models import UserPackage\nfrom src.routers.background import run_in_background\nfrom src.routers.users.db import router as db_router\nfrom src.routers.users.svg import router as svg_router\nfrom src.utils import async_fail_gracefully\n\nrouter = APIRouter()\nrouter.include_router(db_router, prefix=\"/db\")\nrouter.include_router(svg_router, prefix=\"/svg\")\n\n\n\"\"\"\nANALYTICS\n\"\"\"\n\n\n@router.get(\"/{user_id}\", status_code=status.HTTP_200_OK, response_model=Dict[str, Any])\n@async_fail_gracefully\nasync def get_user_endpoint(\n    response: Response,\n    background_tasks: BackgroundTasks,\n    user_id: str,\n    start_date: date = date.today() - timedelta(365),\n    end_date: date = date.today(),\n    timezone_str: str = \"US/Eastern\",\n    no_cache: bool = False,\n) -> Optional[UserPackage]:\n    output, _, background_task = await get_user(\n        user_id, start_date, end_date, no_cache=no_cache\n    )\n    if background_task is not None:\n        # set a background task to update the user\n        background_tasks.add_task(run_in_background, task=background_task)\n\n    return output\n"
  },
  {
    "path": "backend/src/routers/users/svg.py",
    "content": "from datetime import date, timedelta\nfrom typing import Any\n\nfrom fastapi import BackgroundTasks, Response, status\nfrom fastapi.responses import HTMLResponse\nfrom fastapi.routing import APIRouter\n\nfrom src.processing.user import get_top_languages, get_top_repos, svg_base\nfrom src.render import (\n    get_empty_demo_svg,\n    get_loading_svg,\n    get_top_langs_svg,\n    get_top_repos_svg,\n)\nfrom src.routers.background import run_in_background\nfrom src.routers.decorators import svg_fail_gracefully\n\nrouter = APIRouter()\n\n\n@router.get(\n    \"/{user_id}/langs\", status_code=status.HTTP_200_OK, response_class=HTMLResponse\n)\n@svg_fail_gracefully\nasync def get_user_lang_svg(\n    response: Response,\n    background_tasks: BackgroundTasks,\n    user_id: str,\n    start_date: date = date.today() - timedelta(30),\n    end_date: date = date.today(),\n    time_range: str = \"one_year\",\n    timezone_str: str = \"US/Eastern\",\n    use_percent: bool = False,\n    include_private: bool = False,\n    loc_metric: str = \"added\",\n    compact: bool = False,\n    demo: bool = False,\n    no_cache: bool = False,\n    use_animation: bool = True,\n    theme: str = \"classic\",\n) -> Any:\n    output, complete, background_task, time_str = await svg_base(\n        user_id, start_date, end_date, time_range, demo, no_cache\n    )\n    if background_task is not None:\n        # set a background task to update the user\n        background_tasks.add_task(run_in_background, task=background_task)\n\n    # if no data, return loading svg\n    if output is None:\n        return get_loading_svg()\n\n    # get top languages\n    processed, commits_excluded = get_top_languages(output, loc_metric, include_private)\n    return get_top_langs_svg(\n        data=processed,\n        time_str=time_str,\n        use_percent=use_percent,\n        loc_metric=loc_metric,\n        complete=complete,\n        commits_excluded=commits_excluded,\n        compact=compact,\n        use_animation=use_animation,\n        theme=theme,\n    )\n\n\n@router.get(\n    \"/{user_id}/repos\", status_code=status.HTTP_200_OK, response_class=HTMLResponse\n)\n@svg_fail_gracefully\nasync def get_user_repo_svg(\n    response: Response,\n    background_tasks: BackgroundTasks,\n    user_id: str,\n    start_date: date = date.today() - timedelta(30),\n    end_date: date = date.today(),\n    time_range: str = \"one_year\",\n    timezone_str: str = \"US/Eastern\",\n    include_private: bool = False,\n    group: str = \"none\",\n    loc_metric: str = \"added\",\n    demo: bool = False,\n    no_cache: bool = False,\n    use_animation: bool = True,\n    theme: str = \"classic\",\n) -> Any:\n    output, complete, background_task, time_str = await svg_base(\n        user_id, start_date, end_date, time_range, demo, no_cache\n    )\n    if background_task is not None:\n        # set a background task to update the user\n        background_tasks.add_task(run_in_background, task=background_task)\n\n    # if no data, return loading svg\n    if output is None:\n        return get_loading_svg()\n\n    # get top repos\n    processed, commits_excluded = get_top_repos(\n        output, loc_metric, include_private, group\n    )\n    return get_top_repos_svg(\n        data=processed,\n        time_str=time_str,\n        loc_metric=loc_metric,\n        complete=complete,\n        commits_excluded=commits_excluded,\n        use_animation=use_animation,\n        theme=theme,\n    )\n\n\n@router.get(\n    \"/demo\",\n    status_code=status.HTTP_200_OK,\n    response_class=HTMLResponse,\n    include_in_schema=False,\n)\n@svg_fail_gracefully\nasync def get_demo_svg(response: Response, card: str) -> Any:\n    if card == \"langs\":\n        return get_empty_demo_svg(\"Most Used Languages\")\n    elif card == \"repos\":\n        return get_empty_demo_svg(\"Most Contributed Repositories\")\n    else:\n        return get_empty_demo_svg(card)\n"
  },
  {
    "path": "backend/src/routers/wrapped.py",
    "content": "from typing import Any, Dict, Optional\n\nfrom fastapi import APIRouter, Response, status\n\nfrom src.aggregation.layer2 import get_is_valid_user\nfrom src.models import WrappedPackage\nfrom src.processing.wrapped import query_wrapped_user\nfrom src.utils import async_fail_gracefully\n\nrouter = APIRouter()\n\n\n@router.get(\n    \"/valid/{user_id}\", status_code=status.HTTP_200_OK, response_model=Dict[str, Any]\n)\n@async_fail_gracefully\nasync def check_valid_user(response: Response, user_id: str) -> str:\n    return await get_is_valid_user(user_id)\n\n\n@router.get(\"/{user_id}\", status_code=status.HTTP_200_OK, response_model=Dict[str, Any])\n@async_fail_gracefully\nasync def get_wrapped_user(\n    response: Response, user_id: str, year: int = 2024, no_cache: bool = False\n) -> Optional[WrappedPackage]:\n    valid_user = await get_is_valid_user(user_id)\n    if \"Valid user\" not in valid_user:\n        return WrappedPackage.empty()\n\n    return await query_wrapped_user(user_id, year, no_cache=no_cache)\n"
  },
  {
    "path": "backend/src/utils/__init__.py",
    "content": "from src.utils.alru_cache import alru_cache\nfrom src.utils.decorators import async_fail_gracefully, fail_gracefully\nfrom src.utils.gather import gather\nfrom src.utils.utils import date_to_datetime, format_number, use_time_range\n\n__all__ = [\n    \"alru_cache\",\n    \"async_fail_gracefully\",\n    \"fail_gracefully\",\n    \"gather\",\n    \"date_to_datetime\",\n    \"format_number\",\n    \"use_time_range\",\n]\n"
  },
  {
    "path": "backend/src/utils/alru_cache.py",
    "content": "from datetime import datetime, timedelta\nfrom functools import wraps\nfrom typing import (\n    Any,\n    Awaitable,\n    Callable,\n    Dict,\n    FrozenSet,\n    List,\n    ParamSpec,\n    Tuple,\n    TypeVar,\n)\n\nParam = ParamSpec(\"Param\")\nTOutput = TypeVar(\"TOutput\")\n\nTKey = Tuple[Tuple[Any, ...], FrozenSet[Tuple[str, Any]]]\n\n\ndef alru_cache(max_size: int = 128, ttl: timedelta = timedelta(minutes=1)):\n    def decorator(\n        func: Callable[Param, Awaitable[Tuple[bool, TOutput]]]\n    ) -> Callable[Param, Awaitable[TOutput]]:\n        cache: Dict[TKey, Tuple[datetime, TOutput]] = {}\n        keys: List[TKey] = []\n\n        def in_cache(key: TKey) -> bool:\n            # key not in cache\n            if key not in cache:\n                return False\n\n            # key in cache but expired\n            if datetime.now() - cache[key][0] > ttl:\n                return False\n\n            # key in cache and not expired\n            return True\n\n        def update_cache_and_return(key: TKey, flag: bool, value: TOutput) -> TOutput:\n            # if flag = False, do not update cache and return value\n            if not flag:\n                return value\n\n            # if flag = True, update cache\n            now = datetime.now()\n            cache[key] = (now, value)\n            keys.append(key)\n\n            # remove oldest key if cache is full\n            if len(keys) > max_size:\n                try:\n                    # Should not raise KeyError, but just in case\n                    del cache[keys.pop(0)]\n                except KeyError:\n                    # Already deleted by another thread\n                    pass\n\n            # return value from cache\n            return value  # equal to cache[key][1]\n\n        @wraps(func)\n        async def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> TOutput:\n            key: TKey = tuple(args), frozenset(\n                [(k, v) for k, v in kwargs.items() if k not in [\"no_cache\"]]\n            )\n            if \"no_cache\" in kwargs and kwargs[\"no_cache\"]:\n                (flag, value) = await func(*args, **kwargs)\n                return update_cache_and_return(key, flag, value)\n\n            if in_cache(key):\n                return cache[key][1]\n\n            (flag, value) = await func(*args, **kwargs)\n            return update_cache_and_return(key, flag, value)\n\n        return wrapper\n\n    return decorator\n"
  },
  {
    "path": "backend/src/utils/decorators.py",
    "content": "import logging\nfrom datetime import datetime\nfrom functools import wraps\nfrom typing import Any, Callable, Dict, List\n\nfrom fastapi import Response, status\n\n\ndef fail_gracefully(func: Callable[..., Any]):\n    @wraps(func)  # needed to play nice with FastAPI decorator\n    def wrapper(response: Response, *args: List[Any], **kwargs: Dict[str, Any]) -> Any:\n        start = datetime.now()\n        try:\n            data = func(response, *args, **kwargs)\n            return {\"data\": data, \"message\": \"200 OK\", \"time\": datetime.now() - start}\n        except Exception as e:\n            logging.exception(e)\n            response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR\n            return {\n                \"data\": [],\n                \"message\": f\"Error {str(e)}\",\n                \"time\": datetime.now() - start,\n            }\n\n    return wrapper\n\n\ndef async_fail_gracefully(func: Callable[..., Any]):\n    @wraps(func)  # needed to play nice with FastAPI decorator\n    async def wrapper(\n        response: Response, *args: List[Any], **kwargs: Dict[str, Any]\n    ) -> Any:\n        start = datetime.now()\n        try:\n            data = await func(response, *args, **kwargs)\n            return {\"data\": data, \"message\": \"200 OK\", \"time\": datetime.now() - start}\n        except Exception as e:\n            logging.exception(e)\n            response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR\n            return {\n                \"data\": [],\n                \"message\": f\"Error {str(e)}\",\n                \"time\": datetime.now() - start,\n            }\n\n    return wrapper\n"
  },
  {
    "path": "backend/src/utils/gather.py",
    "content": "import asyncio\nfrom functools import partial, wraps\nfrom typing import Any, Callable, Dict, List\n\n\ndef async_function(func: Callable[..., Any]) -> Callable[..., Any]:\n    @wraps(func)\n    async def run(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:\n        loop = asyncio.get_event_loop()\n        pfunc = partial(func, *args, **kwargs)\n        return await loop.run_in_executor(executor=None, func=pfunc)\n\n    return run\n\n\nasync def gather_with_concurrency(\n    n: int, *tasks: List[Callable[..., Any]]\n) -> List[Any]:\n    semaphore = asyncio.Semaphore(n)\n\n    async def sem_task(task: Callable[..., Any]) -> Any:\n        async with semaphore:\n            return await task  # type: ignore\n\n    return await asyncio.gather(*(sem_task(task) for task in tasks))  # type: ignore\n\n\nasync def gather(\n    funcs: List[Callable[..., Any]],\n    args_dicts: List[Dict[str, Any]],\n    max_threads: int = 5,\n) -> List[Any]:\n    \"\"\"runs the given functions asynchronously\"\"\"\n\n    output: List[Any] = list(\n        await gather_with_concurrency(\n            max_threads,\n            *(async_function(func)(**kwargs) for func, kwargs in zip(funcs, args_dicts))\n        )\n    )\n\n    return output\n"
  },
  {
    "path": "backend/src/utils/utils.py",
    "content": "from datetime import date, datetime, timedelta\nfrom typing import Tuple\n\n\ndef date_to_datetime(\n    dt: date, hour: int = 0, minute: int = 0, second: int = 0\n) -> datetime:\n    return datetime(dt.year, dt.month, dt.day, hour, minute, second)\n\n\n# returns start date, end date, string representing time range\ndef use_time_range(\n    time_range: str, start_date: date, end_date: date\n) -> Tuple[date, date, str]:\n    duration_options = {\n        \"one_month\": (30, \"Past 1 Month\"),\n        \"three_months\": (90, \"Past 3 Months\"),\n        \"six_months\": (180, \"Past 6 Months\"),\n        \"one_year\": (365, \"Past 1 Year\"),\n        \"all_time\": (365 * 10, \"All Time\"),\n    }\n\n    start_str = start_date.strftime(\"X%m/X%d/%Y\").replace(\"X0\", \"X\").replace(\"X\", \"\")\n    end_str = end_date.strftime(\"X%m/X%d/%Y\").replace(\"X0\", \"X\").replace(\"X\", \"\")\n    if end_date == date.today():\n        end_str = \"Present\"\n    time_str = f\"{start_str} - {end_str}\"\n\n    if time_range in duration_options:\n        days, time_str = duration_options[time_range]\n        end_date = date.today()\n        start_date = date.today() - timedelta(days)\n\n    return start_date, end_date, time_str\n\n\ndef format_number(num: int) -> str:\n    if num > 10000:\n        return f\"~{str(num // 1000)}k lines\"\n    elif num > 1000:\n        return f\"~{str(num // 100 / 10)}k lines\"\n    elif num > 100:\n        return f\"~{str(num // 100 * 100)} lines\"\n    else:\n        return \"<100 lines\"\n"
  },
  {
    "path": "backend/tests/__init__.py",
    "content": "from dotenv import find_dotenv, load_dotenv\n\nload_dotenv(find_dotenv(), verbose=True)\n"
  },
  {
    "path": "backend/tests/aggregation/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/aggregation/layer0/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/aggregation/layer0/test_contributions.py",
    "content": "from datetime import date, timedelta\n\nfrom aiounittest.case import AsyncTestCase\n\nfrom src.aggregation.layer0.contributions import get_contributions\nfrom src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID\nfrom src.models import UserContributions\n\n\nclass TestTemplate(AsyncTestCase):\n    async def test_get_contributions(self):\n        response = await get_contributions(\n            user_id=USER_ID,\n            start_date=date.today() - timedelta(days=30),\n            end_date=date.today(),\n            access_token=TOKEN,\n        )\n        self.assertIsInstance(response, UserContributions)\n\n        # TODO: Add further validation\n"
  },
  {
    "path": "backend/tests/aggregation/layer0/test_follows.py",
    "content": "import unittest\n\nfrom src.aggregation.layer0.follows import get_user_follows\nfrom src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID\nfrom src.models import UserFollows\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_follows(self):\n        response = get_user_follows(USER_ID, TOKEN)\n        self.assertIsInstance(response, UserFollows)\n\n        # TODO: Add further validation\n"
  },
  {
    "path": "backend/tests/data/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/data/github/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/data/github/auth/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/data/github/auth/test_main.py",
    "content": "import unittest\n\nfrom src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID\nfrom src.data.github.auth.main import get_unknown_user\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_unknown_user_valid(self):\n        user_id = get_unknown_user(TOKEN)\n        self.assertEqual(user_id, USER_ID)\n\n    def test_get_unknown_user_invalid(self):\n        user_id = get_unknown_user(\"\")\n        self.assertEqual(user_id, None)\n\n    # TODO: test authenticate()\n"
  },
  {
    "path": "backend/tests/data/github/graphql/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/data/github/graphql/test_commits.py",
    "content": "import unittest\n\nfrom src.constants import TEST_NODE_IDS as NODE_IDS, TEST_TOKEN as TOKEN\nfrom src.data.github.graphql import RawCommit, get_commits\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_commits(self):\n        node_ids = get_commits(access_token=TOKEN, node_ids=NODE_IDS, catch_errors=True)\n\n        # assert returns equal number of commits\n        self.assertIsInstance(node_ids, list)\n        self.assertEqual(len(node_ids), len(NODE_IDS))\n\n        self.assertIsInstance(node_ids[0], RawCommit)\n\n    def test_get_commits_invalid_access_token(self):\n        node_ids = get_commits(access_token=\"\", node_ids=NODE_IDS, catch_errors=True)\n\n        # assert returns list of Nones\n        self.assertIsInstance(node_ids, list)\n        self.assertEqual(len(node_ids), len(NODE_IDS))\n        self.assertIsInstance(node_ids[0], type(None))\n\n    def test_get_commits_invalid_node_ids(self):\n        node_ids = get_commits(\n            access_token=TOKEN,\n            node_ids=[NODE_IDS[0], \"\", NODE_IDS[1]],\n            catch_errors=True,\n        )\n\n        self.assertIsInstance(node_ids[0], RawCommit)\n        self.assertIsInstance(node_ids[1], type(None))\n        self.assertIsInstance(node_ids[2], RawCommit)\n"
  },
  {
    "path": "backend/tests/data/github/graphql/test_repo.py",
    "content": "import unittest\n\nfrom src.constants import (\n    TEST_REPO as REPO,\n    TEST_TOKEN as TOKEN,\n    TEST_USER_ID as USER_ID,\n)\nfrom src.data.github.graphql import RawRepo, get_repo\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_repo(self):\n        repo: RawRepo = get_repo(  # type: ignore\n            access_token=TOKEN, owner=USER_ID, repo=REPO, catch_errors=True\n        )\n\n        # assert returns equal number of commits\n        self.assertIsInstance(repo, RawRepo)\n        self.assertEqual(repo.is_private, False)\n        self.assertGreater(repo.fork_count, 0)\n        self.assertGreater(repo.stargazer_count, 0)\n\n    def test_get_repo_invalid_access_token(self):\n        repo = get_repo(access_token=\"\", owner=USER_ID, repo=REPO, catch_errors=True)\n\n        # assert returns None\n        self.assertIsInstance(repo, type(None))\n\n    def test_get_commits_invalid_args(self):\n        repo = get_repo(\n            access_token=TOKEN, owner=\"abc123\", repo=REPO, catch_errors=True\n        )\n\n        self.assertIsInstance(repo, type(None))\n"
  },
  {
    "path": "backend/tests/data/github/graphql/test_user_contribs.py",
    "content": "import unittest\nfrom datetime import datetime, timedelta\n\nfrom src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID\nfrom src.data.github.graphql import (\n    RawCalendar,\n    RawEvents,\n    get_user_contribution_calendar,\n    get_user_contribution_events,\n)\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_user_contribution_calendar(self):\n        response = get_user_contribution_calendar(\n            user_id=USER_ID,\n            access_token=TOKEN,\n            start_date=datetime.now() - timedelta(days=30),\n            end_date=datetime.now(),\n        )\n\n        self.assertIsInstance(response, RawCalendar)\n\n    def test_get_user_contribution_events(self):\n        response = get_user_contribution_events(\n            user_id=USER_ID,\n            access_token=TOKEN,\n            start_date=datetime.now() - timedelta(days=30),\n            end_date=datetime.now(),\n        )\n\n        self.assertIsInstance(response, RawEvents)\n"
  },
  {
    "path": "backend/tests/data/github/graphql/test_user_follows.py",
    "content": "import unittest\n\nfrom src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID\nfrom src.data.github.graphql import RawFollows, get_user_followers, get_user_following\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_user_followers(self):\n        response = get_user_followers(user_id=USER_ID, access_token=TOKEN)\n        self.assertIsInstance(response, RawFollows)\n\n        response = get_user_followers(user_id=USER_ID, access_token=TOKEN, first=1)\n        self.assertLessEqual(len(response.nodes), 1)\n\n    def test_get_user_following(self):\n        response = get_user_following(user_id=USER_ID, access_token=TOKEN)\n        self.assertIsInstance(response, RawFollows)\n\n        response = get_user_following(user_id=USER_ID, access_token=TOKEN, first=1)\n        self.assertLessEqual(len(response.nodes), 1)\n"
  },
  {
    "path": "backend/tests/data/github/rest/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/data/github/rest/test_commit.py",
    "content": "import unittest\n\nfrom src.constants import (\n    TEST_REPO as REPO,\n    TEST_SHA as SHA,\n    TEST_TOKEN as TOKEN,\n    TEST_USER_ID as USER_ID,\n)\nfrom src.data.github.rest import RawCommitFile, get_commit_files\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_commit_files(self):\n        commits = get_commit_files(\n            access_token=TOKEN, owner=USER_ID, repo=REPO, sha=SHA\n        )\n        self.assertIsInstance(commits, list)\n        self.assertIsInstance(commits[0], RawCommitFile)  # type: ignore\n\n    def test_get_commit_files_invalid_access_token(self):\n        repo = get_commit_files(access_token=\"\", owner=USER_ID, repo=REPO, sha=SHA)\n        self.assertEqual(repo, None)\n\n    def test_get_commit_files_invalid_args(self):\n        repo = get_commit_files(access_token=TOKEN, owner=\"abc123\", repo=REPO, sha=SHA)\n        self.assertEqual(repo, None)\n"
  },
  {
    "path": "backend/tests/data/github/rest/test_repo.py",
    "content": "import unittest\n\nfrom src.constants import (\n    TEST_REPO as REPO,\n    TEST_TOKEN as TOKEN,\n    TEST_USER_ID as USER_ID,\n)\nfrom src.data.github.rest import RawCommit, get_repo_commits\n\n\nclass TestTemplate(unittest.TestCase):\n    def test_get_repo_commits(self):\n        commits = get_repo_commits(access_token=TOKEN, owner=USER_ID, repo=REPO)\n        self.assertIsInstance(commits, list)\n        self.assertIsInstance(commits[0], RawCommit)\n\n    def test_get_repo_commits_invalid_access_token(self):\n        repo = get_repo_commits(access_token=\"\", owner=USER_ID, repo=REPO)\n        self.assertEqual(repo, [])\n\n    def test_get_repo_commits_invalid_args(self):\n        repo = get_repo_commits(access_token=TOKEN, owner=\"abc123\", repo=REPO)\n        self.assertEqual(repo, [])\n"
  },
  {
    "path": "backend/tests/utils/__init__.py",
    "content": ""
  },
  {
    "path": "backend/tests/utils/test_alru_cache.py",
    "content": "from asyncio import sleep\nfrom datetime import timedelta\nfrom typing import Tuple\n\nfrom aiounittest.case import AsyncTestCase\n\nfrom src.utils import alru_cache\n\n\nclass TestTemplate(AsyncTestCase):\n    async def test_basic_alru_cache(self):\n        count = 0\n\n        @alru_cache()\n        async def f(x: int) -> Tuple[bool, int]:\n            nonlocal count\n            count += 1\n            return (True, x)\n\n        assert count == 0\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(2) == 2\n        assert count == 2\n        assert await f(2) == 2\n        assert count == 2\n        assert await f(1) == 1\n        assert count == 2\n\n    async def test_alru_cache_with_flag(self):\n        count = 0\n\n        @alru_cache()\n        async def f(x: int) -> Tuple[bool, int]:\n            nonlocal count\n            count += 1\n            return (count % 2 == 0, x)\n\n        assert count == 0\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(1) == 1\n        assert count == 2\n        assert await f(2) == 2\n        assert count == 3\n        assert await f(3) == 3\n        assert count == 4\n        assert await f(3) == 3\n        assert count == 4\n\n    async def test_alru_cache_with_maxsize(self):\n        count = 0\n\n        @alru_cache(max_size=2)\n        async def f(x: int) -> Tuple[bool, int]:\n            nonlocal count\n            count += 1\n            return (True, x)\n\n        assert count == 0\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(2) == 2\n        assert count == 2\n        assert await f(3) == 3\n        assert count == 3\n        assert await f(1) == 1\n        assert count == 4\n\n    async def test_alru_cache_with_ttl(self):\n        count = 0\n\n        @alru_cache(ttl=timedelta(milliseconds=1))\n        async def f(x: int) -> Tuple[bool, int]:\n            nonlocal count\n            count += 1\n            return (True, x)\n\n        assert count == 0\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(1) == 1\n        assert count == 1\n        await sleep(0.01)\n        assert await f(1) == 1\n        assert count == 2\n\n    async def test_alru_cache_with_no_cache(self):\n        count = 0\n\n        @alru_cache()\n        async def f(x: int, no_cache: bool = False) -> Tuple[bool, int]:\n            nonlocal count\n            count += 1\n            return (True, x)\n\n        assert count == 0\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(1) == 1\n        assert count == 1\n        assert await f(2) == 2\n        assert count == 2\n        assert await f(2, no_cache=True) == 2\n        assert count == 3\n        assert await f(3) == 3\n        assert count == 4\n        assert await f(3, no_cache=False) == 3\n        assert count == 4\n"
  },
  {
    "path": "backend/transfer_mongodb.bash",
    "content": "if [ $# -eq 0 ]; then\n    echo \"Usage: $0 <mongodb_password>\"\n    exit 1\nfi\n\n# Export\nmongoexport --uri \"mongodb+srv://backend.aqlpb.mongodb.net/\" --db dev_backend --collection secrets --username root --password \"$1\" > ./dev_secrets.json\nmongoexport --uri \"mongodb+srv://backend.aqlpb.mongodb.net/\" --db dev_backend --collection users --username root --password \"$1\" > ./dev_users.json\nmongoexport --uri \"mongodb+srv://backend.aqlpb.mongodb.net/\" --db dev_backend --collection user_months --username root --password \"$1\" > ./dev_user_months.json\n\nmongoexport --uri \"mongodb+srv://backend.aqlpb.mongodb.net/\" --db prod_backend --collection secrets --username root --password \"$1\" > ./prod_secrets.json\nmongoexport --uri \"mongodb+srv://backend.aqlpb.mongodb.net/\" --db prod_backend --collection users --username root --password \"$1\" > ./prod_users.json\nmongoexport --uri \"mongodb+srv://backend.aqlpb.mongodb.net/\" --db prod_backend --collection user_months --username root --password \"$1\" > ./prod_user_months.json\n\n# Import\nmongoimport --uri \"mongodb+srv://backend2.e50j8dp.mongodb.net/\" --db dev_backend --collection secrets --username root --password \"$1\" < ./dev_secrets.json\nmongoimport --uri \"mongodb+srv://backend2.e50j8dp.mongodb.net/\" --db dev_backend --collection users --username root --password \"$1\" < ./dev_users.json\nmongoimport --uri \"mongodb+srv://backend2.e50j8dp.mongodb.net/\" --db dev_backend --collection user_months --username root --password \"$1\" < ./dev_user_months.json\n\nmongoimport --uri \"mongodb+srv://backend2.e50j8dp.mongodb.net/\" --db prod_backend --collection secrets --username root --password \"$1\" < ./prod_secrets.json\nmongoimport --uri \"mongodb+srv://backend2.e50j8dp.mongodb.net/\" --db prod_backend --collection users --username root --password \"$1\" < ./prod_users.json\nmongoimport --uri \"mongodb+srv://backend2.e50j8dp.mongodb.net/\" --db prod_backend --collection user_months --username root --password \"$1\" < ./prod_user_months.json\n\n# Remove\nrm ./dev_secrets.json\nrm ./dev_users.json\nrm ./dev_user_months.json\n\nrm ./prod_secrets.json\nrm ./prod_users.json\nrm ./prod_user_months.json"
  },
  {
    "path": "docs/API.md",
    "content": "# GitHub Trends API\n\nGitHub Trends provides two methods to access GitHub Trends data: the Website Workflow at githubtrends.io and the API Workflow described below.\n\n## Available Cards\n\nAfter authenticating with either the public or private workflow (see below), users can create the following cards to display their GitHub Trends data:\n\n- **[Languages Card](https://github.com/avgupta456/github-trends/blob/main/docs/API.md#languages-card)**: See your top languages over a given time interval, based on all commits to personal and open-source repositories.\n\n- **[Repositories Card](https://github.com/avgupta456/github-trends/blob/main/docs/API.md#repositories-card)**: See your top repositories based on lines of code contributed over a given time period. This includes both personal and open-source repositories.\n\n# Authentication\n\nYou will need to create an account with GitHub Trends to create cards. The account is used to assosciate queries to the GitHub API made on your behalf with your GitHub account's API quota. We use less than 5% of your quota in almost all scenarios. There are two levels of authentication possible:\n\n- Public Workflow: The Public Workflow asks for read-only permission to public information. This will allow us to analyze your public contributions and repositories only.\n- Private Workflow: The Private Workflow asks for read and write permission to public and private information. This will allow us to analyze your entire contribution history. See [the FAQ](https://github.com/avgupta456/github-trends/blob/main/docs/FAQ.md) for further information.\n\nYou will only need to authenticate once with GitHub Trends. Subsequent requests will use your stored access token.\n\nFor the public workflow, visit\n\n```md\nhttps://api.githubtrends.io/auth/signup/public\n```\n\nFor the private workflow, visit\n\n```md\nhttps://api.githubtrends.io/auth/signup/private\n```\n\nYou will be prompted to allow access, and (hopefully) redirected to a success screen.\n\nIf you have previously authenticated with the public workflow, you can upgrade to the private workflow by using the private link. If you would like to delete your account, go to your GitHub settings and revoke the access token.\n\n# Languages Card\n\nSee your top languages over a given time interval, based on all commits to personal and open-source repositories. Your top five languages will be displayed. Due to the approximations used internally, LOC metrics will be rounded to the nearest 100 lines.\n\nAfter authentication, visit\n\n```md\nhttps://api.githubtrends.io/user/svg/{user_id}/langs\n```\n\n## Customization\n\nThe following customization options are available:\n\n| Option            | Description                                                                                                                                | Default     |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |\n| `time_range`      | Specifies the time range to query statistics for. Valid options are `one_month`, `three_months`, `six_months`, `one_year`, and `all_time`. | `one_month` |\n| `include_private` | Determines if private contributions are included (requires private workflow).                                                              | `false`     |\n| `compact`         | Determines if compact layout is used (forces percentages over LOC)                                                                         | `false`     |\n| `use_percent`     | Valid if `compact=false`, determines if line of code (default) or percentages are displayed.                                               | `false`     |\n| `loc_metric`      | Options are LOC added (`added`) and LOC changed (`changed`).                                                                               | `added`     |\n| `theme`           | Theme to use for the card. See [docs/THEME.md](https://github.com/avgupta456/github-trends/blob/main/docs/THEME.md) for options.           | `classic`   |\n\nCustomizations can be appended to the endpoint, separated first with `?` and subsequently with `&`.\n\n## Example\n\nEndpoint: `https://api.githubtrends.io/user/svg/avgupta456/langs?time_range=three_months&include_private=true&compact=true`\n\n[![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?time_range=three_months&include_private=true&compact=true)](https://githubtrends.io)\n\n# Repositories Card\n\nAfter authentication, visit\n\n```md\nhttps://api.githubtrends.io/user/svg/{user_id}/repos\n```\n\n## Customization\n\nThe following customization options are available:\n\n| Option            | Description                                                                                                                                | Default     |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |\n| `time_range`      | Specifies the time range to query statistics for. Valid options are `one_month`, `three_months`, `six_months`, `one_year`, and `all_time`. | `one_month` |\n| `include_private` | Determines if private contributions are included (requires private workflow).                                                              | `false`     |\n| `group`           | Options are `none` (default), `other` (group all other repos together), and `private` (force private repos to be grouped)                  | `none`      |\n| `use_percent`     | Valid if `compact=false`, determines if line of code (default) or percentages are displayed.                                               | `false`     |\n| `loc_metric`      | Options are LOC added (`added`) and LOC changed (`changed`).                                                                               | `added`     |\n| `theme`           | Theme to use for the card. See [docs/THEME.md](https://github.com/avgupta456/github-trends/blob/main/docs/THEME.md) for options.           | `classic`   |\n\nCustomizations can be appended to the endpoint, separated first with `?` and subsequently with `&`.\n\n## Example\n\nEndpoint: `https://api.githubtrends.io/user/svg/avgupta456/repos?time_range=one_year&include_private=true&group=private&loc_metric=changed&theme=dark`\n\n[![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/repos?time_range=one_year&include_private=true&group=private&loc_metric=changed&theme=dark)](https://githubtrends.io)\n"
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "content": "# GitHub Trends\n\nIf you are interested in contributing to GitHub Trends, take a look through the codebase and at the open issues. Follow the guide below to set up your local environment, and contact Abhijit Gupta at `avgupta456@gmail.com` if you have any questions or need additional permissions. Thank you in advance for contributing!\n\n## Local Development\n\nFirst, copy `backend/.env-template` into `backend/.env` and fill in the missing variables. Similarly, copy `frontend/.env-template` into `frontend/.env` and fill in the missing variables. Create a Google Cloud Platform service account and include the key in `backend/gcloud_key.json`. Then run:\n\nWith Python3.11, install the dependencies from `backend/requirements.txt` and run `yarn start`.\n\nWith Node16 and Yarn, install the dependencies from `frontend/package.json` and run on a separate terminal window `yarn start-trends`.\n\n## Testing\n\nCreate a pull request and let GitHub Actions run. Alternatively, explore `.github/backend.yaml` and `.github/frontend.yaml` to run tests locally. Backend coverage must increase for PRs to be merged.\n"
  },
  {
    "path": "docs/FAQ.md",
    "content": "# FAQ\n\nThe FAQ is in progress. Reach out if you have any unanswered questions or concerns.\n\n---\n\n**Question**: Does GitHub Trends have access to my private code contributions?\n\n**Answer**: GitHub Trends requires an OAuth access token to make requests on your behalf. The standard public workflow creates a token with read-only access to strictly public information. **This access token can not view or edit any private contributions**.\n\nAlternatively, users can use the private workflow which creates a token with read and write access to private information. Although GitHub Trends only uses it's read access, GitHub does not allow read-only private access (see [an open issue from 2015](https://github.com/jollygoodcode/jollygoodcode.github.io/issues/6)). While one may scan the repository to confirm this statement, there are inherent security risks to this overallocation. If this poses an issue to you, please use the public workflow instead.\n\n**Question**: How can I display my images side by side?\n\n**Answer**: Use HTML (credit: [github-readme-stats](https://github.com/anuraghazra/github-readme-stats#quick-tip-align-the-repo-cards))\n\n```\n<a href=\"https://githubtrends.io\">\n  <img align=\"center\" src=\"https://api.githubtrends.io/user/svg/avgupta456/langs\" />\n</a>\n<a href=\"https://githubtrends.io\">\n  <img align=\"center\" src=\"https://api.githubtrends.io/user/svg/avgupta456/repos\" />\n</a>\n```\n\n**Question**: How can I see my stats without giving GitHub Trends my access token?\n\n**Answer**: You will need to run the code locally. Clone the repository, navigate to the `backend` folder, install the dependencies (`pip install -r requirements.txt`), and then run the following script:\n\n```bash\npython ./scripts/local.py --user_id=USER_ID --access_token=ACCESS_TOKEN --start_date=2023-01-01 --end_date=2023-01-31 --output_dir=OUTPUT_DIR\n```\n\nThe script will output the raw and processed JSONs into the output directory specified.\n\n**Question**: What if I find a bug, or want to contribute?\n\n**Answer**: Raise an [issue](https://github.com/avgupta456/github-trends/issues/new) or [pull request](https://github.com/avgupta456/github-trends/compare) through GitHub. I would be happy to discuss and implement any suggestions or improvements.\n"
  },
  {
    "path": "docs/THEME.md",
    "content": "The following themes are available for all GitHub Trends cards:\n\n| Themes                                                                                                                |                                                                                                                    |                                                                                                                            |\n| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |\n| [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?theme=classic)](https://githubtrends.io)  | [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?theme=dark)](https://githubtrends.io)  | [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?theme=bright_lights)](https://githubtrends.io) |\n| [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?theme=rosettes)](https://githubtrends.io) | [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?theme=ferns)](https://githubtrends.io) | [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?theme=synthwaves)](https://githubtrends.io)    |\n"
  },
  {
    "path": "frontend/.env-template",
    "content": "REACT_APP_PROD=false\n\nREACT_APP_CLIENT_ID=abc123"
  },
  {
    "path": "frontend/.eslintrc.js",
    "content": "module.exports = {\n  env: {\n    browser: true,\n    es6: true,\n  },\n  extends: ['airbnb', 'plugin:prettier/recommended'],\n  parserOptions: {\n    ecmaFeatures: {\n      jsx: true,\n    },\n    ecmaVersion: 2020,\n    sourceType: 'module',\n  },\n  plugins: ['react', 'prettier'],\n  rules: {\n    'react/jsx-filename-extension': 'off',\n    'react/forbid-prop-types': 'off',\n    'react/destructuring-assignment': 'off',\n    'import/prefer-default-export': 'off',\n    'react/function-component-definition': 'off',\n    'react/no-unstable-nested-components': 'off',\n    'jsx-a11y/control-has-associated-label': 'off',\n    'no-console': 'off',\n    radix: 'off',\n    'prettier/prettier': [\n      'error',\n      {\n        endOfLine: 'auto',\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# custom\npublic/index.html"
  },
  {
    "path": "frontend/.prettierrc.js",
    "content": "module.exports = {\n  semi: true,\n  trailingComma: 'all',\n  singleQuote: true,\n  printWidth: 80,\n  tabWidth: 2,\n};\n"
  },
  {
    "path": "frontend/.yarnrc",
    "content": "network-timeout 500000"
  },
  {
    "path": "frontend/README.md",
    "content": "# Frontend\n\n## Installation\n\n```\nyarn install\n```\n\n## Run Locally\n\n```\nyarn start-trends\nyarn start-wrapped\n```\n\n## Build\n\n```\nyarn build-trends\nyarn build-wrapped\n```\n\nThen, just commit on the main branch (Vercel takes care of the rest)\n\n## Adding a Secret\n\nUpdate .env, .env-template, and Vercel.\n"
  },
  {
    "path": "frontend/deploy/Dockerfile",
    "content": "FROM node:16-alpine\n\nWORKDIR /frontend\n\nENV PATH /frontend/node_modules/.bin:$PATH\n\nCOPY ../package.json ../yarn.lock /frontend/\nRUN yarn install --network-timeout 100000\n\nCOPY ../ /frontend\n\nCMD [\"yarn\", \"start\"]"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.23.2\",\n    \"@nivo/bar\": \"^0.83.0\",\n    \"@nivo/calendar\": \"^0.83.0\",\n    \"@nivo/core\": \"^0.83.0\",\n    \"@nivo/pie\": \"^0.83.0\",\n    \"@nivo/radar\": \"^0.83.0\",\n    \"@nivo/swarmplot\": \"^0.83.0\",\n    \"@nivo/tooltip\": \"^0.83.0\",\n    \"axios\": \"^1.6.1\",\n    \"daisyui\": \"2.31.0\",\n    \"downloadjs\": \"^1.4.7\",\n    \"html-to-image\": \"^1.11.11\",\n    \"moment\": \"^2.29.4\",\n    \"prop-types\": \"^15.8.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-icons\": \"^4.11.0\",\n    \"react-loading-skeleton\": \"^3.3.1\",\n    \"react-redux\": \"^8.1.3\",\n    \"react-router-dom\": \"^6.18.0\",\n    \"react-scripts\": \"^5.0.1\",\n    \"react-select\": \"^5.8.0\",\n    \"react-spinners\": \"^0.13.8\",\n    \"react-toastify\": \"^9.1.3\",\n    \"react-typist\": \"^2.0.5\",\n    \"react-typist-loop\": \"0.0.5\",\n    \"redux\": \"^4.2.1\",\n    \"save-svg-as-png\": \"^1.4.17\",\n    \"uuid\": \"^9.0.1\"\n  },\n  \"devDependencies\": {\n    \"autoprefixer\": \"^10.4.16\",\n    \"eslint\": \"8.53.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-plugin-import\": \"^2.29.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-prettier\": \"^5.0.1\",\n    \"eslint-plugin-react\": \"7.33.2\",\n    \"eslint-plugin-react-hooks\": \"4.6.0\",\n    \"postcss\": \"^8.4.31\",\n    \"prettier\": \"^3.0.3\",\n    \"tailwindcss\": \"^3.3.5\"\n  },\n  \"scripts\": {\n    \"setup-trends\": \"cp ./public/trends.html ./public/index.html\",\n    \"setup-wrapped\": \"cp ./public/wrapped.html ./public/index.html\",\n    \"start-trends\": \"yarn setup-trends && REACT_APP_MODE=trends react-scripts start\",\n    \"start-wrapped\": \"yarn setup-wrapped && REACT_APP_MODE=wrapped PORT=3001 react-scripts start\",\n    \"build-trends\": \"yarn setup-trends && REACT_APP_MODE=trends react-scripts build\",\n    \"build-wrapped\": \"yarn setup-wrapped && REACT_APP_MODE=wrapped react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "frontend/public/_redirects",
    "content": "/* /index.html 200"
  },
  {
    "path": "frontend/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "frontend/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "frontend/public/trends.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta name=\"title\" content=\"Github Trends\" />\n    <meta\n      name=\"description\"\n      content=\"Discover and display statistics about your code contributions. Level up your GitHub profile readme with customizable cards including LOC statistics!\"\n    />\n\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:url\" content=\"https://githubtrends.io/\" />\n    <meta property=\"og:title\" content=\"Github Trends\" />\n    <meta\n      property=\"og:description\"\n      content=\"Discover and display statistics about your code contributions. Level up your GitHub profile readme with customizable cards including LOC statistics!\"\n    />\n    <meta\n      property=\"og:image\"\n      content=\"https://www.githubtrends.io/preview_trends.png\"\n    />\n\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:creator\" content=\"@avgupta456\" />\n    <meta name=\"twitter:url\" content=\"https://githubtrends.io\" />\n    <meta name=\"twitter:title\" content=\"Github Trends\" />\n    <meta\n      name=\"twitter:description\"\n      content=\"Discover and display statistics about your code contributions. Level up your GitHub profile readme with customizable cards including LOC statistics!\"\n    />\n    <meta\n      name=\"twitter:image\"\n      content=\"https://www.githubtrends.io/preview_trends.png\"\n    />\n\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n\n    <!-- Global site tag (gtag.js) - Google Analytics -->\n    <script\n      async\n      src=\"https://www.googletagmanager.com/gtag/js?id=G-FT096DX9CK\"\n    ></script>\n    <script>\n      window.dataLayer = window.dataLayer || [];\n      function gtag() {\n        dataLayer.push(arguments);\n      }\n      gtag('js', new Date());\n\n      gtag('config', 'G-FT096DX9CK');\n    </script>\n\n    <title>GitHub Trends</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/public/wrapped.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta name=\"title\" content=\"Github Wrapped\" />\n    <meta\n      name=\"description\"\n      content=\"Reflect on your year of coding with GitHub Wrapped. See lines of code written, broken down by language, repository, time of day, and more!\"\n    />\n\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:url\" content=\"https://githubwrapped.io/\" />\n    <meta property=\"og:title\" content=\"Github Wrapped\" />\n    <meta\n      property=\"og:description\"\n      content=\"Reflect on your year of coding with GitHub Wrapped. See lines of code written, broken down by language, repository, time of day, and more!\"\n    />\n    <meta\n      property=\"og:image\"\n      content=\"https://www.githubwrapped.io/preview_wrapped.png\"\n    />\n\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:creator\" content=\"@avgupta456\" />\n    <meta name=\"twitter:url\" content=\"https://githubwrapped.io\" />\n    <meta name=\"twitter:title\" content=\"Github Wrapped\" />\n    <meta\n      name=\"twitter:description\"\n      content=\"Reflect on your year of coding with GitHub Wrapped. See lines of code written, broken down by language, repository, time of day, and more!\"\n    />\n    <meta\n      name=\"twitter:image\"\n      content=\"https://www.githubwrapped.io/preview_wrapped.png\"\n    />\n\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n\n    <!-- Global site tag (gtag.js) - Google Analytics -->\n    <script\n      async\n      src=\"https://www.googletagmanager.com/gtag/js?id=G-FT096DX9CK\"\n    ></script>\n    <script>\n      window.dataLayer = window.dataLayer || [];\n      function gtag() {\n        dataLayer.push(arguments);\n      }\n      gtag('js', new Date());\n\n      gtag('config', 'G-FT096DX9CK');\n    </script>\n\n    <title>GitHub Wrapped</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/src/api/index.js",
    "content": "import {\n  setUserKey,\n  authenticate,\n  getUserMetadata,\n  deleteAccount,\n} from './user';\n\nimport { getWrapped } from './wrapped';\n\nexport { setUserKey, authenticate, getUserMetadata, deleteAccount, getWrapped };\n"
  },
  {
    "path": "frontend/src/api/user.js",
    "content": "import axios from 'axios';\nimport { v4 as uuidv4 } from 'uuid';\n\nimport { BACKEND_URL } from '../constants';\n\nconst URL_PREFIX = BACKEND_URL;\n\nconst setUserKey = async (code) => {\n  try {\n    const key = uuidv4();\n    const fullUrl = `${URL_PREFIX}/auth/web/set_user_key/${code}/${key}`;\n    await axios.post(fullUrl);\n    return key;\n  } catch (error) {\n    console.error(error);\n    return '';\n  }\n};\n\nconst authenticate = async (code, privateAccess) => {\n  try {\n    const fullUrl = `${URL_PREFIX}/auth/web/login/${code}?private_access=${privateAccess}`;\n    const result = await axios.post(fullUrl);\n    return result.data.data;\n  } catch (error) {\n    console.error(error);\n    return '';\n  }\n};\n\nconst getUserMetadata = async (userId) => {\n  try {\n    const fullUrl = `${URL_PREFIX}/user/db/get/metadata/${userId}`;\n    const result = await axios.get(fullUrl);\n    return result.data.data;\n  } catch (error) {\n    console.error(error);\n    return '';\n  }\n};\n\nconst deleteAccount = async (userId, userKey) => {\n  try {\n    const fullUrl = `${URL_PREFIX}/auth/web/delete/${userId}?user_key=${userKey}`;\n    const result = await axios.get(fullUrl);\n    return result.data; // no decorator\n  } catch (error) {\n    console.error(error);\n    return '';\n  }\n};\n\nexport { setUserKey, authenticate, getUserMetadata, deleteAccount };\n"
  },
  {
    "path": "frontend/src/api/wrapped.js",
    "content": "/* eslint-disable no-return-await */\n\nimport axios from 'axios';\n\nimport { BACKEND_URL } from '../constants';\n\nconst URL_PREFIX = `${BACKEND_URL}/wrapped`;\n\nconst getValidUser = async (userId) => {\n  try {\n    const fullUrl = `${URL_PREFIX}/valid/${userId}`;\n    const result = await axios.get(fullUrl);\n    return result.data.data;\n  } catch (error) {\n    return null;\n  }\n};\n\nconst getWrapped = async (userId, year) => {\n  try {\n    const fullUrl = `${URL_PREFIX}/${userId}?year=${year}`;\n    const result = await axios.get(fullUrl);\n    return result.data.data;\n  } catch (error) {\n    return null;\n  }\n};\n\nexport { getWrapped, getValidUser };\n"
  },
  {
    "path": "frontend/src/assets/notes.txt",
    "content": "2022: Generated laptop mockups using deviceshots.com\n2023: Used https://www.anthonyboyd.graphics/mockups/m2-macbook-air-mockup/ and Adobe Express"
  },
  {
    "path": "frontend/src/components/Card/Card.js",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport PropTypes from 'prop-types';\n\nimport { BACKEND_URL } from '../../constants';\n\nimport SVG from './SVG';\nimport { classnames } from '../../utils';\n\nexport const Image = ({ imageSrc, compact }) => {\n  const userId = useSelector((state) => state.user.userId);\n  const fullImageSrc = `${BACKEND_URL}/user/svg/${userId}/${imageSrc}`;\n\n  return (\n    <div className=\"relative h-full w-full relative\">\n      <SVG\n        className=\"object-cover h-full w-full\"\n        url={fullImageSrc}\n        compact={compact}\n      />\n    </div>\n  );\n};\n\nImage.propTypes = {\n  imageSrc: PropTypes.string.isRequired,\n  compact: PropTypes.bool,\n};\n\nImage.defaultProps = {\n  compact: false,\n};\n\nexport const Card = ({ title, description, imageSrc, selected, compact }) => {\n  return (\n    <div\n      className={classnames(\n        'p-6 rounded border-2',\n        selected\n          ? 'border-blue-500 bg-blue-50'\n          : 'border-gray-200 bg-white hover:bg-gray-50',\n      )}\n    >\n      <h2 className=\"text-xl font-medium title-font text-gray-900\">{title}</h2>\n      <p className=\"text-base leading-relaxed mt-2 mb-4\">{description}</p>\n      <Image imageSrc={imageSrc} compact={compact} />\n    </div>\n  );\n};\n\nCard.propTypes = {\n  title: PropTypes.string.isRequired,\n  description: PropTypes.string.isRequired,\n  imageSrc: PropTypes.string.isRequired,\n  selected: PropTypes.bool,\n  compact: PropTypes.bool,\n};\n\nCard.defaultProps = {\n  selected: false,\n  compact: false,\n};\n"
  },
  {
    "path": "frontend/src/components/Card/SVG.js",
    "content": "/* eslint-disable react/jsx-props-no-spreading */\n/* eslint-disable react/no-danger */\n\nimport React, { useEffect, useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport Skeleton from 'react-loading-skeleton';\nimport 'react-loading-skeleton/dist/skeleton.css';\n\nconst SvgInline = (props) => {\n  const [svg, setSvg] = useState(null);\n\n  // eslint-disable-next-line no-unused-vars\n  const [loaded, setLoaded] = useState(false);\n\n  let url = `${props.url.split('?')[0]}?cache=${Date.now()}`;\n  if (props.url.split('?').length > 1) {\n    url += `&${props.url.split('?')[1]}`;\n  }\n\n  useEffect(() => {\n    setLoaded(false);\n    fetch(url)\n      .then((res) => res.text())\n      .then(setSvg)\n      .then(() => setLoaded(true))\n      .catch((e) => console.error(e));\n  }, [props.url]);\n\n  if (props.forceLoading || !loaded) {\n    if (props.compact) {\n      return <Skeleton style={{ paddingBottom: '58%' }} />;\n    }\n    return <Skeleton style={{ paddingBottom: '95%' }} />;\n  }\n\n  if (props.compact) {\n    return (\n      <div\n        className={props.className}\n        dangerouslySetInnerHTML={{\n          __html: `<svg id=\"svg-card\" viewBox=\"0 0 300 175\">${svg}</svg>`,\n        }}\n      />\n    );\n  }\n\n  return (\n    <div\n      className={props.className}\n      dangerouslySetInnerHTML={{\n        __html: `<svg id=\"svg-card\" viewBox=\"0 0 300 285\">${svg}</svg>`,\n      }}\n    />\n  );\n};\n\nSvgInline.propTypes = {\n  className: PropTypes.any,\n  url: PropTypes.string.isRequired,\n  forceLoading: PropTypes.bool,\n  compact: PropTypes.bool,\n};\n\nSvgInline.defaultProps = {\n  className: '',\n  forceLoading: false,\n  compact: false,\n};\n\nexport default SvgInline;\n"
  },
  {
    "path": "frontend/src/components/Card/index.js",
    "content": "import SvgInline from './SVG';\nimport { Card, Image } from './Card';\n\nexport { Card, Image, SvgInline };\n"
  },
  {
    "path": "frontend/src/components/Generic/Button.js",
    "content": "/* eslint-disable react/jsx-props-no-spreading */\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { classnames } from '../../utils';\n\nconst Button = (props) => {\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      className={classnames(\n        props.className,\n        'border-0 py-2 px-6 inline-flex focus:outline-none rounded-sm text-lg',\n      )}\n    >\n      {props.children}\n    </button>\n  );\n};\n\nButton.propTypes = {\n  className: PropTypes.string,\n  children: PropTypes.node.isRequired,\n};\n\nButton.defaultProps = {\n  className: '',\n};\n\nexport default Button;\n"
  },
  {
    "path": "frontend/src/components/Generic/Checkbox.js",
    "content": "/* eslint-disable jsx-a11y/interactive-supports-focus */\n/* eslint-disable jsx-a11y/click-events-have-key-events */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nconst Checkbox = ({ question, variable, setVariable, disabled }) => {\n  return (\n    <div\n      className=\"flex inline-row mt-4\"\n      onClick={() => setVariable(!variable)}\n      role=\"button\"\n    >\n      <input\n        type=\"checkbox\"\n        disabled={disabled}\n        checked={variable && !disabled ? 'checked' : ''}\n        className=\"checkbox mr-2\"\n        onChange={() => setVariable(!variable)}\n      />\n      {question}\n    </div>\n  );\n};\n\nCheckbox.propTypes = {\n  question: PropTypes.string.isRequired,\n  variable: PropTypes.bool.isRequired,\n  setVariable: PropTypes.func.isRequired,\n  disabled: PropTypes.bool,\n};\n\nCheckbox.defaultProps = {\n  disabled: false,\n};\n\nexport default Checkbox;\n"
  },
  {
    "path": "frontend/src/components/Generic/Input.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { classnames } from '../../utils';\n\n// options is of form [{value: '', label: '', disabled: true/false}]\nconst Input = ({\n  options,\n  selectedOption,\n  setSelectedOption,\n  disabled,\n  className,\n}) => {\n  return (\n    <select\n      className={classnames(\n        'text-gray-700 bg-white select select-sm w-40 rounded-sm mt-4',\n        className,\n      )}\n      value={selectedOption.label}\n      onChange={(e) =>\n        setSelectedOption(\n          options.find((item) => item.label === e.target.value),\n          // eslint-disable-next-line prettier/prettier\n        )}\n      disabled={disabled}\n    >\n      {options.map((option) => (\n        <option\n          key={option.value}\n          disabled={option.disabled}\n          className={classnames(\n            option.label === selectedOption.label && 'bg-blue-200',\n          )}\n        >\n          {option.label}\n        </option>\n      ))}\n    </select>\n  );\n};\n\nInput.propTypes = {\n  options: PropTypes.arrayOf(\n    PropTypes.shape({\n      value: PropTypes.string.isRequired,\n      label: PropTypes.string.isRequired,\n      disabled: PropTypes.bool,\n    }),\n  ).isRequired,\n  selectedOption: PropTypes.shape({\n    value: PropTypes.string.isRequired,\n    label: PropTypes.string.isRequired,\n  }).isRequired,\n  setSelectedOption: PropTypes.func.isRequired,\n  disabled: PropTypes.bool,\n  className: PropTypes.string,\n};\n\nInput.defaultProps = {\n  disabled: false,\n  className: '',\n};\n\nexport default Input;\n"
  },
  {
    "path": "frontend/src/components/Generic/index.js",
    "content": "import Button from './Button';\nimport Checkbox from './Checkbox';\nimport Input from './Input';\n\nexport { Button, Checkbox, Input };\n"
  },
  {
    "path": "frontend/src/components/Home/CheckboxSection.js",
    "content": "/* eslint-disable jsx-a11y/interactive-supports-focus */\n/* eslint-disable jsx-a11y/click-events-have-key-events */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport Section from './Section';\nimport { Checkbox } from '../Generic';\n\nconst CheckboxSection = ({\n  title,\n  text,\n  question,\n  variable,\n  setVariable,\n  disabled,\n}) => {\n  return (\n    <Section title={title}>\n      <p>{text}</p>\n      <Checkbox\n        question={question}\n        variable={variable}\n        setVariable={setVariable}\n        disabled={disabled}\n      />\n    </Section>\n  );\n};\n\nCheckboxSection.propTypes = {\n  title: PropTypes.string.isRequired,\n  text: PropTypes.string.isRequired,\n  question: PropTypes.string.isRequired,\n  variable: PropTypes.bool.isRequired,\n  setVariable: PropTypes.func.isRequired,\n  disabled: PropTypes.bool,\n};\n\nCheckboxSection.defaultProps = {\n  disabled: false,\n};\n\nexport default CheckboxSection;\n"
  },
  {
    "path": "frontend/src/components/Home/DateRangeSection.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport Section from './Section';\nimport { Input } from '../Generic';\n\nconst DateRangeSection = ({\n  selectedTimeRange,\n  setSelectedTimeRange,\n  // eslint-disable-next-line no-unused-vars\n  privateAccess,\n}) => {\n  const timeRangeOptions = [\n    { id: 1, label: 'Past 1 Month', disabled: false, value: 'one_month' },\n    {\n      id: 2,\n      label: 'Past 3 Months',\n      disabled: false,\n      value: 'three_months',\n    },\n    { id: 2, label: 'Past 6 Months', disabled: false, value: 'six_months' },\n    { id: 3, label: 'Past 1 Year', disabled: false, value: 'one_year' },\n    // { id: 4, label: 'All Time', disabled: !privateAccess, value: 'all_time' },\n    { id: 4, label: 'All Time', disabled: true, value: 'all_time' },\n  ];\n\n  const selectedOption = selectedTimeRange || timeRangeOptions[2];\n\n  return (\n    <Section title=\"Date Range\">\n      <p>Select the date range for statistics.</p>\n      <Input\n        options={timeRangeOptions}\n        selectedOption={selectedOption}\n        setSelectedOption={setSelectedTimeRange}\n      />\n    </Section>\n  );\n};\n\nDateRangeSection.propTypes = {\n  selectedTimeRange: PropTypes.object.isRequired,\n  setSelectedTimeRange: PropTypes.func.isRequired,\n  privateAccess: PropTypes.bool.isRequired,\n};\n\nexport default DateRangeSection;\n"
  },
  {
    "path": "frontend/src/components/Home/Progress.js",
    "content": "/* eslint-disable react/no-array-index-key */\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport {\n  FaArrowLeft as LeftArrowIcon,\n  FaArrowRight as RightArrowIcon,\n} from 'react-icons/fa';\n\nimport { classnames } from '../../utils';\n\nconst ProgressSection = ({ num, item, passed, onClick }) => {\n  return (\n    <button\n      className={classnames(\n        'w-1/4 flex flex-col mx-2 p-2 border-t-4 cursor-pointer',\n        passed ? 'border-blue-500' : 'border-gray-400',\n      )}\n      type=\"button\"\n      onClick={onClick}\n    >\n      <div\n        className={classnames(\n          'text-lg font-bold',\n          passed ? 'text-blue-500' : 'text-gray-400',\n        )}\n      >\n        {`Step ${num + 1}`}\n      </div>\n      <div className={classnames(passed ? 'text-gray-700' : 'text-gray-400')}>\n        {item}\n      </div>\n    </button>\n  );\n};\n\nProgressSection.propTypes = {\n  num: PropTypes.number.isRequired,\n  item: PropTypes.string.isRequired,\n  passed: PropTypes.bool.isRequired,\n  onClick: PropTypes.func.isRequired,\n};\n\nconst ProgressBar = ({ items, currItem, setCurrItem }) => {\n  const leftDisabled = currItem === 0;\n  const rightDisabled = currItem === items.length - 1;\n\n  return (\n    <div className=\"w-full flex items-center\">\n      <LeftArrowIcon\n        className={classnames(\n          'w-8 h-8',\n          leftDisabled\n            ? 'text-gray-300 cursor-not-allowed'\n            : 'text-gray-700 cursor-pointer',\n        )}\n        onClick={() => setCurrItem(currItem - 1)}\n      />\n      <div className=\"px-2 flex-grow flex flex-row\">\n        {items.map((item, index) => {\n          return (\n            <ProgressSection\n              num={index}\n              key={index}\n              item={item}\n              passed={currItem >= index}\n              onClick={() => setCurrItem(index)}\n            />\n          );\n        })}\n      </div>\n      <RightArrowIcon\n        className={classnames(\n          'w-8 h-8',\n          rightDisabled\n            ? 'text-gray-300 cursor-not-allowed'\n            : 'text-gray-700 cursor-pointer',\n        )}\n        onClick={() => setCurrItem(currItem + 1)}\n      />\n    </div>\n  );\n};\n\nProgressBar.propTypes = {\n  items: PropTypes.array.isRequired,\n  currItem: PropTypes.number.isRequired,\n  setCurrItem: PropTypes.func.isRequired,\n};\n\nexport default ProgressBar;\n"
  },
  {
    "path": "frontend/src/components/Home/Section.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { HiOutlineLightningBolt as LightningIcon } from 'react-icons/hi';\n\nconst Section = (props) => {\n  return (\n    <div className=\"flex relative pb-12\">\n      <div className=\"h-full w-10 absolute inset-0 flex items-center justify-center\">\n        <div className=\"h-full w-1 bg-gray-200 pointer-events-none\" />\n      </div>\n      <div className=\"flex-shrink-0 w-10 h-10 rounded-full bg-blue-500 inline-flex items-center justify-center text-white relative z-10\">\n        <LightningIcon className=\"w-5 h-5\" />\n      </div>\n\n      <div className=\"flex-grow pl-4\">\n        <h2 className=\"font-medium title-font text-sm text-gray-900 mb-1 tracking-wider\">\n          {props.title}\n        </h2>\n        {props.children}\n      </div>\n    </div>\n  );\n};\n\nSection.propTypes = {\n  title: PropTypes.string,\n  children: PropTypes.node,\n};\n\nSection.defaultProps = {\n  title: 'Test',\n  children: <p className=\"leading-relaxed\">This is a test!</p>,\n};\n\nexport default Section;\n"
  },
  {
    "path": "frontend/src/components/Home/index.js",
    "content": "import ProgressBar from './Progress';\nimport CheckboxSection from './CheckboxSection';\nimport DateRangeSection from './DateRangeSection';\n\nexport { ProgressBar, CheckboxSection, DateRangeSection };\n"
  },
  {
    "path": "frontend/src/components/Preview/Preview.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport PropTypes from 'prop-types';\n\nimport {\n  FaArrowRight as ArrowRightIcon,\n  FaArrowLeft as ArrowLeftIcon,\n} from 'react-icons/fa';\n\nimport { classnames } from '../../utils';\n\nconst Preview = ({ pages, details, showArrows }) => {\n  const totalPages = pages.length;\n  const [page, setPage] = useState(0);\n\n  const prevPage = () => {\n    setPage((page - 1 + totalPages) % totalPages);\n  };\n\n  const nextPage = () => {\n    setPage((page + 1 + totalPages) % totalPages);\n  };\n\n  useEffect(() => {\n    const interval = setInterval(nextPage, 5000);\n    return () => clearInterval(interval);\n  }, [page]);\n\n  return (\n    <div className=\"w-7/8 mx-auto p-8 rounded-sm\">\n      <br />\n      <div className=\"flex items-center\">\n        {showArrows && (\n          <ArrowLeftIcon\n            className=\"mr-4 w-8 h-8 text-gray-300 hover:text-gray-600\"\n            onClick={prevPage}\n          />\n        )}\n        <img\n          src={pages[page]}\n          alt=\"preview\"\n          className={showArrows ? 'w-3/4 mx-auto' : 'w-full'}\n        />\n        {showArrows && (\n          <ArrowRightIcon\n            className=\"ml-4 w-8 h-8 text-gray-300 hover:text-gray-600\"\n            onClick={nextPage}\n          />\n        )}\n      </div>\n      <br />\n      <p\n        className={classnames(\n          'w-full text-center',\n          'text-sm lg:text-lg xl:text-xl 2xl:text-2xl',\n        )}\n      >\n        {details[page]}\n      </p>\n    </div>\n  );\n};\n\nPreview.propTypes = {\n  pages: PropTypes.arrayOf(PropTypes.any).isRequired,\n  details: PropTypes.arrayOf(PropTypes.string).isRequired,\n  showArrows: PropTypes.bool,\n};\n\nPreview.defaultProps = {\n  showArrows: true,\n};\n\nexport default Preview;\n"
  },
  {
    "path": "frontend/src/components/Preview/index.js",
    "content": "import Preview from './Preview';\n\nexport default Preview;\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Organization.js",
    "content": "/* eslint-disable jsx-a11y/mouse-events-have-key-events */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport { classnames } from '../../utils';\n\nconst WrappedSection = (props) => {\n  return (\n    <div className=\"w-full h-auto flex flex-wrap mb-8\">\n      {props.useTitle && (\n        <p className=\"w-screen p-2 text-2xl lg:text-3xl\">{props.title}</p>\n      )}\n      {props.children}\n    </div>\n  );\n};\n\nWrappedSection.propTypes = {\n  useTitle: PropTypes.bool,\n  title: PropTypes.string,\n  children: PropTypes.node.isRequired,\n};\n\nWrappedSection.defaultProps = {\n  useTitle: true,\n  title: '',\n};\n\nconst WrappedCard = (props) => {\n  return (\n    <div\n      className=\"w-full h-full p-1\"\n      onMouseOver={props.onMouseOver}\n      onMouseOut={props.onMouseOut}\n    >\n      <div\n        className={classnames(\n          'shadow rounded-sm bg-gray-100 w-full h-full p-4 flex flex-col justify-center',\n          props.className,\n        )}\n      >\n        {props.children}\n      </div>\n    </div>\n  );\n};\n\nWrappedCard.propTypes = {\n  children: PropTypes.node.isRequired,\n  className: PropTypes.string,\n  onMouseOver: PropTypes.func,\n  onMouseOut: PropTypes.func,\n};\n\nWrappedCard.defaultProps = {\n  className: '',\n  onMouseOver: () => {},\n  onMouseOut: () => {},\n};\n\nexport { WrappedSection, WrappedCard };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/Bar.js",
    "content": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { WrappedCard } from '../Organization';\nimport { BarGraph } from '../Templates';\n\nconst monthNames = [\n  'Jan',\n  'Feb',\n  'March',\n  'April',\n  'May',\n  'June',\n  'July',\n  'Aug',\n  'Sep',\n  'Oct',\n  'Nov',\n  'Dec',\n];\n\nconst dayNames = [\n  'Sunday',\n  'Monday',\n  'Tuesday',\n  'Wednesday',\n  'Thursday',\n  'Friday',\n  'Saturday',\n];\n\nconst BarMonth = ({ data, downloadLoading }) => {\n  const newData = data?.month_data?.months || [];\n\n  // eslint-disable-next-line no-unused-vars\n  const [displayContribs, setDisplayContribs] = useState(true);\n\n  return (\n    <div className=\"h-96 w-full\">\n      <WrappedCard>\n        <div className=\"flex\">\n          <div className=\"flex-grow\">\n            <p className=\"text-xl font-semibold\">Contributions by Month</p>\n            <p>\n              {displayContribs ? 'By Contribution Count' : 'By LOC Changed'}\n            </p>\n          </div>\n          {!downloadLoading && (\n            <div className=\"flex-shrink-0\">\n              <button\n                type=\"button\"\n                className=\"bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center\"\n                onClick={() => setDisplayContribs(!displayContribs)}\n              >\n                <span>Toggle</span>\n              </button>\n            </div>\n          )}\n        </div>\n        {displayContribs ? (\n          <BarGraph\n            data={newData}\n            labels={monthNames}\n            xTitle=\"Month\"\n            type=\"contribs\"\n            getLabel={(d) => d.contribs}\n            legendText=\"Contributions\"\n          />\n        ) : (\n          <BarGraph\n            data={newData}\n            labels={monthNames}\n            xTitle=\"Month\"\n            type=\"loc_changed\"\n            getLabel={(d) => d.formatted_loc_changed.split(' ')[0]}\n            legendText=\"LOC Changed\"\n          />\n        )}\n      </WrappedCard>\n    </div>\n  );\n};\n\nBarMonth.propTypes = {\n  data: PropTypes.object.isRequired,\n  downloadLoading: PropTypes.bool.isRequired,\n};\n\nconst BarDay = ({ data, downloadLoading }) => {\n  const newData = data?.day_data?.days || [];\n\n  const [displayContribs, setDisplayContribs] = useState(true);\n\n  return (\n    <div className=\"h-96 w-full\">\n      <WrappedCard>\n        <div className=\"flex\">\n          <div className=\"flex-grow\">\n            <p className=\"text-xl font-semibold\">Contributions by Day</p>\n            <p>\n              {displayContribs ? 'By Contribution Count' : 'By LOC Changed'}\n            </p>\n          </div>\n          {!downloadLoading && (\n            <div className=\"flex-shrink-0\">\n              <button\n                type=\"button\"\n                className=\"bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center\"\n                onClick={() => setDisplayContribs(!displayContribs)}\n              >\n                <span>Toggle</span>\n              </button>\n            </div>\n          )}\n        </div>\n        {displayContribs ? (\n          <BarGraph\n            data={newData}\n            labels={dayNames}\n            xTitle=\"Day\"\n            type=\"contribs\"\n            getLabel={(d) => d.contribs}\n            legendText=\"Contributions\"\n          />\n        ) : (\n          <BarGraph\n            data={newData}\n            labels={dayNames}\n            xTitle=\"Day\"\n            type=\"loc_changed\"\n            getLabel={(d) => d.formatted_loc_changed.split(' ')[0]}\n            legendText=\"LOC Changed\"\n          />\n        )}\n      </WrappedCard>\n    </div>\n  );\n};\n\nBarDay.propTypes = {\n  data: PropTypes.object.isRequired,\n  downloadLoading: PropTypes.bool.isRequired,\n};\n\nexport { BarMonth, BarDay };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/Calendar.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ResponsiveCalendar } from '@nivo/calendar';\n\nimport { Input } from '../../Generic';\nimport { theme, scale } from '../Templates/theme';\nimport { WrappedCard } from '../Organization';\n\nconst Calendar = ({\n  data,\n  startDate,\n  endDate,\n  highlightDays,\n  highlightColors,\n  downloadLoading,\n}) => {\n  const newData = data?.calendar_data?.days || [];\n\n  const valueOptions = [\n    { value: 'contribs', label: 'Contributions', disabled: false },\n    { value: 'commits', label: 'Commits', disabled: false },\n    { value: 'issues', label: 'Issues', disabled: false },\n    { value: 'prs', label: 'Pull Requests', disabled: false },\n    { value: 'reviews', label: 'Reviews', disabled: false },\n  ];\n\n  const [value, setValue] = React.useState(valueOptions[0]);\n\n  const numEvents = Array.isArray(newData)\n    ? newData.reduce((acc, x) => acc + x[value.value], 0)\n    : 0;\n\n  let c = 0;\n  const max = Math.max(...newData.map((x) => x[value.value]));\n  const quantiles = [\n    Math.floor(max * 0.25),\n    Math.floor(max * 0.5),\n    Math.floor(max * 0.75),\n    max,\n  ];\n\n  const colorScaleFn = (x) => {\n    const count = (c % 365) + 1;\n    c += 1;\n\n    const myColorScale = highlightDays.includes(count)\n      ? highlightColors\n      : scale;\n\n    if (x === 0) {\n      return myColorScale[0];\n    }\n    if (x <= quantiles[0]) {\n      return myColorScale[1];\n    }\n    if (x <= quantiles[1]) {\n      return myColorScale[2];\n    }\n    if (x <= quantiles[2]) {\n      return myColorScale[3];\n    }\n    return myColorScale[4];\n  };\n\n  return (\n    <div className=\"w-full\">\n      <WrappedCard>\n        <div className=\"h-6 flex justify-between items-center\">\n          <p className=\"text-lg lg:text-xl font-semibold\">\n            Contribution Calendar\n          </p>\n          {!downloadLoading && (\n            <Input\n              className=\"hidden lg:block w-48 border-2 border-gray-300\"\n              options={valueOptions}\n              selectedOption={value}\n              setSelectedOption={setValue}\n            />\n          )}\n        </div>\n        <div className=\"flex flex-col h-48\">\n          <p className=\"lg:text-lg\">{`${numEvents} ${value.label}`}</p>\n          {Array.isArray(newData) && newData.length > 0 ? (\n            <ResponsiveCalendar\n              theme={theme}\n              data={newData.map((item) => ({\n                day: item.day,\n                value: item[value.value],\n              }))}\n              from={startDate}\n              to={endDate}\n              emptyColor=\"#EBEDF0\"\n              colors={['#9BE9A8', '#40C463', '#30A14E', '#216E39']}\n              margin={{ top: 10, right: 0, bottom: 0, left: 0 }}\n              monthBorderColor=\"#ffffff\"\n              dayBorderWidth={2}\n              dayBorderColor=\"#ffffff\"\n              colorScale={colorScaleFn}\n            />\n          ) : (\n            <div className=\"w-full h-full flex items-center justify-center\">\n              No data to show\n            </div>\n          )}\n        </div>\n      </WrappedCard>\n    </div>\n  );\n};\n\nCalendar.propTypes = {\n  data: PropTypes.object.isRequired,\n  startDate: PropTypes.string.isRequired,\n  endDate: PropTypes.string.isRequired,\n  highlightDays: PropTypes.arrayOf(PropTypes.number),\n  highlightColors: PropTypes.arrayOf(PropTypes.string).isRequired,\n  downloadLoading: PropTypes.bool.isRequired,\n};\n\nCalendar.defaultProps = {\n  highlightDays: [],\n};\n\nexport default Calendar;\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/Numeric.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { WrappedCard } from '../Organization';\n\nconst numericPropTypes = {\n  num: PropTypes.any,\n  label: PropTypes.string.isRequired,\n};\n\nconst numericDefaultProps = {\n  num: 'N/A',\n};\n\nconst NumericPlusLOC = ({ num, label }) => {\n  return (\n    <WrappedCard>\n      <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold w-full text-center text-green-600\">{`+${num}`}</p>\n      <p className=\"2xl:text-lg w-full text-center text-green-600\">{label}</p>\n    </WrappedCard>\n  );\n};\n\nNumericPlusLOC.propTypes = numericPropTypes;\nNumericPlusLOC.defaultProps = numericDefaultProps;\n\nconst NumericMinusLOC = ({ num, label }) => {\n  return (\n    <WrappedCard>\n      <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold w-full text-center text-red-600\">{`-${num}`}</p>\n      <p className=\"2xl:text-lg w-full text-center text-red-600\">{label}</p>\n    </WrappedCard>\n  );\n};\n\nNumericMinusLOC.propTypes = numericPropTypes;\nNumericMinusLOC.defaultProps = numericDefaultProps;\n\nconst NumericBothLOC = ({ num1, num2, label }) => {\n  return (\n    <WrappedCard>\n      <div className=\"flex justify-center\">\n        <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center text-green-600\">\n          {num1}\n        </p>\n        <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold mx-2\">/</p>\n        <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center text-red-600\">\n          {num2}\n        </p>\n      </div>\n      <p className=\"2xl:text-lg w-full text-center\">{label}</p>\n    </WrappedCard>\n  );\n};\n\nNumericBothLOC.propTypes = {\n  num1: PropTypes.any,\n  num2: PropTypes.any,\n  label: PropTypes.string.isRequired,\n};\n\nNumericBothLOC.defaultProps = {\n  num1: 'N/A',\n  num2: 'N/A',\n};\n\nconst NumericBestDay = ({\n  num,\n  date,\n  label,\n  className,\n  onMouseOver,\n  onMouseOut,\n}) => {\n  return (\n    <WrappedCard\n      className={className}\n      onMouseOver={onMouseOver}\n      onMouseOut={onMouseOut}\n    >\n      <div className=\"h-24 mb-2 flex flex-col justify-center\">\n        <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center text-green-600\">\n          {num} Contributions\n        </p>\n        <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold text-center\">\n          on {date}\n        </p>\n      </div>\n      <p className=\"text-lg 2xl:text-xl w-full text-center\">{label}</p>\n    </WrappedCard>\n  );\n};\n\nNumericBestDay.propTypes = {\n  num: PropTypes.number.isRequired,\n  date: PropTypes.string.isRequired,\n  label: PropTypes.string.isRequired,\n  className: PropTypes.string,\n  onMouseOver: PropTypes.func,\n  onMouseOut: PropTypes.func,\n};\n\nNumericBestDay.defaultProps = {\n  className: '',\n  onMouseOver: () => {},\n  onMouseOut: () => {},\n};\n\nexport { NumericPlusLOC, NumericMinusLOC, NumericBothLOC, NumericBestDay };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/Pie.js",
    "content": "/* eslint-disable react/jsx-curly-newline */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { PieChart } from '../Templates';\nimport { WrappedCard } from '../Organization';\n\nconst PieLangs = ({ data, downloadLoading }) => {\n  const [useLOCAdded, setUseLOCAdded] = React.useState(false);\n\n  const metric = useLOCAdded ? 'added' : 'changed';\n  const newData = data?.lang_data?.[`langs_${metric}`] || [];\n\n  return (\n    <div className=\"h-96 w-full\">\n      <WrappedCard>\n        <div className=\"flex\">\n          <div className=\"flex-grow\">\n            <p className=\"text-xl font-semibold\">Most Used Languages</p>\n            <p>{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}</p>\n          </div>\n          {!downloadLoading && (\n            <div className=\"flex-shrink-0\">\n              <button\n                type=\"button\"\n                className=\"bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center\"\n                onClick={() => setUseLOCAdded(!useLOCAdded)}\n              >\n                <span>Toggle</span>\n              </button>\n            </div>\n          )}\n        </div>\n        <PieChart\n          data={newData}\n          getArcLinkLabel={(e) => e.data.label}\n          getFormattedValue={(e) => e.formatted_value}\n          colors={{ datum: 'data.color' }}\n        />\n      </WrappedCard>\n    </div>\n  );\n};\n\nPieLangs.propTypes = {\n  data: PropTypes.object.isRequired,\n  downloadLoading: PropTypes.bool.isRequired,\n};\n\nconst PieRepos = ({ data, downloadLoading }) => {\n  const [useLOCAdded, setUseLOCAdded] = React.useState(false);\n\n  const metric = useLOCAdded ? 'added' : 'changed';\n  const newData = data?.repo_data?.[`repos_${metric}`] || [];\n\n  return (\n    <div className=\"h-96 w-full\">\n      <WrappedCard>\n        <div className=\"flex\">\n          <div className=\"flex-grow\">\n            <p className=\"text-xl font-semibold\">Most Active Repositories</p>\n            <p>{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}</p>\n          </div>\n          {!downloadLoading && (\n            <div className=\"flex-shrink-0\">\n              <button\n                type=\"button\"\n                className=\"bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center\"\n                onClick={() => setUseLOCAdded(!useLOCAdded)}\n              >\n                <span>Toggle</span>\n              </button>\n            </div>\n          )}\n        </div>\n        <PieChart\n          data={newData}\n          getArcLinkLabel={({ data: { label } }) => {\n            if (label && label.includes('/')) {\n              return label.split('/')[1].replace('repository', 'private');\n            }\n            return label;\n          }}\n          getFormattedValue={(e) => e.formatted_value}\n          colors={{ scheme: 'category10' }}\n        />\n      </WrappedCard>\n    </div>\n  );\n};\n\nPieRepos.propTypes = {\n  data: PropTypes.object.isRequired,\n  downloadLoading: PropTypes.bool.isRequired,\n};\n\nexport { PieLangs, PieRepos };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/Radar.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ResponsiveRadar } from '@nivo/radar';\n\nimport { WrappedCard } from '../Organization';\n\n// eslint-disable-next-line no-unused-vars\nconst Radar = ({ data }) => {\n  const commits = data?.numeric_data?.contribs?.commits || 0;\n  const issues = data?.numeric_data?.contribs?.issues || 0;\n  const prs = data?.numeric_data?.contribs?.prs || 0;\n  const reviews = data?.numeric_data?.contribs?.reviews || 0;\n\n  const tempData = [\n    {\n      name: 'Commits',\n      count: Math.log(1 + commits),\n    },\n    {\n      name: 'Issues',\n      count: Math.log(1 + issues),\n    },\n    {\n      name: 'Pull Requests',\n      count: Math.log(1 + prs),\n    },\n    {\n      name: 'Reviews',\n      count: Math.log(1 + reviews),\n    },\n  ];\n\n  return (\n    <div className=\"h-96 w-full\">\n      <WrappedCard>\n        <p className=\"text-xl font-semibold\">Contributions by Type</p>\n        <p>Log Scale</p>\n        <ResponsiveRadar\n          data={tempData}\n          keys={['count']}\n          indexBy=\"name\"\n          valueFormat={(d) => Math.round(Math.exp(d) - 1)}\n          margin={{ top: 30, right: 50, bottom: 30, left: 60 }}\n          dotSize={10}\n          colors={{ scheme: 'category10' }}\n          blendMode=\"multiply\"\n          motionConfig=\"wobbly\"\n        />\n      </WrappedCard>\n    </div>\n  );\n};\n\nRadar.propTypes = {\n  data: PropTypes.object.isRequired,\n};\n\nexport default Radar;\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/Swarm.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { SwarmPlot } from '../Templates';\n\nconst formatYAxis = (value) => {\n  if (value === 3600 * 12) {\n    return 'Noon';\n  }\n  if (value === 3600 * 24) {\n    return 'Midnight';\n  }\n  let hours = Math.floor(value / 3600);\n  const suffix = hours % 24 >= 12 ? 'PM' : 'AM';\n  hours = hours % 12 === 0 ? 12 : hours % 12;\n  const minutes = String(Math.floor((value % 3600) / 60 / 10) * 10);\n  const displayHour = String(hours).padStart(2, '0');\n  const displayMinute = String(minutes).padStart(2, '0');\n  return `${displayHour}:${displayMinute} ${suffix}`;\n};\n\nconst SwarmDay = ({ data }) => {\n  let newData = data?.timestamp_data?.contribs || [];\n  newData = newData.map((d, i) => {\n    return {\n      ...d,\n      groupById: 0,\n      id: i,\n    };\n  });\n\n  return (\n    <SwarmPlot\n      header=\"Contributions by Time\"\n      data={newData}\n      groupBy=\"groupById\"\n      groups={[0]}\n      legend=\"\"\n      formatXAxis={() => ''}\n      formatYAxis={formatYAxis}\n    />\n  );\n};\n\nSwarmDay.propTypes = {\n  data: PropTypes.object.isRequired,\n};\n\nexport { SwarmDay };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Specifics/index.js",
    "content": "import Calendar from './Calendar';\n\nexport * from './Bar';\nexport * from './Numeric';\nexport * from './Pie';\nexport * from './Swarm';\nexport { Calendar };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Templates/Bar.js",
    "content": "/* eslint-disable react/jsx-curly-newline */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ResponsiveBar } from '@nivo/bar';\n\nimport { theme } from './theme';\n\nconst BarGraph = ({ data, labels, xTitle, type, getLabel, legendText }) => {\n  const maxData = Math.max(...data.map((d) => d[type]));\n  const minData = Math.min(\n    ...data.filter((d) => d.index < 11).map((d) => d[type]),\n  );\n\n  const getColor = (d) => {\n    // eslint-disable-next-line no-nested-ternary\n    return d.value === maxData\n      ? '#2BA02C'\n      : d.value === minData\n      ? '#D62728'\n      : '#468CBF';\n  };\n\n  if (!(Array.isArray(data) && data.length > 0)) {\n    return (\n      <div className=\"w-full h-full flex items-center justify-center\">\n        No data to show\n      </div>\n    );\n  }\n\n  return (\n    <ResponsiveBar\n      theme={theme}\n      colors={getColor}\n      data={data}\n      indexBy=\"index\"\n      keys={[type]}\n      margin={{ top: 30, right: 0, bottom: 40, left: 80 }}\n      padding={0.3}\n      layout=\"vertical\"\n      axisTop={null}\n      axisRight={null}\n      axisBottom={{\n        tickSize: 5,\n        tickPadding: 5,\n        tickRotation: 0,\n        legend: xTitle,\n        legendPosition: 'middle',\n        legendOffset: 32,\n        format: (value) => labels[value],\n      }}\n      axisLeft={{\n        tickSize: 5,\n        tickPadding: 5,\n        tickRotation: 0,\n        legend: legendText,\n        legendPosition: 'middle',\n        legendOffset: -60,\n      }}\n      label={(d) => getLabel(d.data)}\n      labelSkipWidth={12}\n      labelSkipHeight={12}\n      labelTextColor=\"#fff\"\n      tooltip={() => null}\n    />\n  );\n};\n\nBarGraph.propTypes = {\n  data: PropTypes.array.isRequired,\n  labels: PropTypes.array.isRequired,\n  xTitle: PropTypes.string.isRequired,\n  type: PropTypes.string.isRequired,\n  getLabel: PropTypes.func.isRequired,\n  legendText: PropTypes.string.isRequired,\n};\n\nexport default BarGraph;\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Templates/Numeric.js",
    "content": "/* eslint-disable jsx-a11y/mouse-events-have-key-events */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ResponsivePie } from '@nivo/pie';\n\nimport { WrappedCard } from '../Organization';\n\nconst Numeric = ({ num, label }) => {\n  return (\n    <WrappedCard>\n      <p className=\"text-2xl 2xl:text-3xl 3xl:text-4xl font-bold w-full text-center\">\n        {num || 'N/A'}\n      </p>\n      <p className=\"2xl:text-lg w-full text-center\">{label}</p>\n    </WrappedCard>\n  );\n};\n\nNumeric.propTypes = {\n  num: PropTypes.any,\n  label: PropTypes.string.isRequired,\n};\n\nNumeric.defaultProps = {\n  num: 'N/A',\n};\n\nconst NumericOutOf = ({\n  num,\n  outOf,\n  format,\n  label,\n  color,\n  className,\n  onMouseOver,\n  onMouseOut,\n}) => {\n  // eslint-disable-next-line react/prop-types\n  const CenteredMetric = ({ dataWithArc, centerX, centerY }) => {\n    let total = 0;\n    // eslint-disable-next-line react/prop-types\n    dataWithArc.forEach((datum) => {\n      total += datum.id === '1' ? datum.value : 0;\n    });\n\n    return (\n      <text\n        x={centerX}\n        y={centerY}\n        textAnchor=\"middle\"\n        dominantBaseline=\"central\"\n        className=\"text-2xl 2xl:text-3xl font-bold\"\n      >\n        {format(total)}\n      </text>\n    );\n  };\n  return (\n    <WrappedCard\n      className={className}\n      onMouseOver={onMouseOver}\n      onMouseOut={onMouseOut}\n    >\n      <div className=\"w-full h-24 mb-2\">\n        <ResponsivePie\n          data={[\n            { id: '1', value: num, color },\n            { id: '2', value: outOf - num, color: '#d1d5db' },\n          ]}\n          innerRadius={0.8}\n          enableArcLabels={false}\n          enableArcLinkLabels={false}\n          colors={{ datum: 'data.color' }}\n          layers={['arcs', CenteredMetric]}\n          tooltip={() => null}\n        />\n      </div>\n      <p className=\"text-lg 2xl:text-xl w-full text-center\">{label}</p>\n    </WrappedCard>\n  );\n};\n\nNumericOutOf.propTypes = {\n  num: PropTypes.number.isRequired,\n  outOf: PropTypes.number.isRequired,\n  format: PropTypes.func,\n  label: PropTypes.string.isRequired,\n  color: PropTypes.string,\n  className: PropTypes.string,\n  onMouseOver: PropTypes.func,\n  onMouseOut: PropTypes.func,\n};\n\nNumericOutOf.defaultProps = {\n  format: (x) => x,\n  color: '#30A14E',\n  className: '',\n  onMouseOver: () => {},\n  onMouseOut: () => {},\n};\n\nexport { Numeric, NumericOutOf };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Templates/Pie.js",
    "content": "/* eslint-disable react/jsx-curly-newline */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ResponsivePie } from '@nivo/pie';\n\nimport { theme } from './theme';\n\nconst PieChart = ({ data, getArcLinkLabel, getFormattedValue, colors }) => {\n  if (!(Array.isArray(data) && data.length > 0)) {\n    return (\n      <div className=\"w-full h-full flex items-center justify-center\">\n        No data to show\n      </div>\n    );\n  }\n  return (\n    <ResponsivePie\n      theme={theme}\n      data={data}\n      margin={{ top: 20, right: 40, bottom: 20, left: 40 }}\n      innerRadius={0.4}\n      padAngle={0.7}\n      cornerRadius={3}\n      activeOuterRadiusOffset={8}\n      borderWidth={1}\n      borderColor={{ from: 'color', modifiers: [['darker', 0.2]] }}\n      // Arc Link Settings\n      arcLinkLabel={(e) => getArcLinkLabel(e)}\n      arcLinkLabelsSkipAngle={45}\n      arcLinkLabelsTextOffset={0}\n      arcLinkLabelsTextColor={{ from: 'color' }}\n      arcLinkLabelsDiagonalLength={5}\n      arcLinkLabelsStraightLength={5}\n      arcLinkLabelsThickness={0}\n      // Arc Label Settings\n      arcLabel={(e) => getFormattedValue(e.data)}\n      arcLabelsSkipAngle={45}\n      arcLabelsTextColor=\"#fff\"\n      // Tooltip\n      tooltip={({ datum }) => (\n        <div\n          style={{\n            fontSize: '14px',\n            padding: 6,\n            color: datum.color,\n            background: '#fff',\n            boxShadow: '0px 0px 10px rgba(0, 0, 0, 0.1)',\n          }}\n        >\n          <strong>{datum.label}</strong>\n          {`: ${getFormattedValue(datum.data)}`}\n        </div>\n      )}\n      colors={colors}\n    />\n  );\n};\n\nPieChart.propTypes = {\n  data: PropTypes.array.isRequired,\n  getArcLinkLabel: PropTypes.func.isRequired,\n  getFormattedValue: PropTypes.func.isRequired,\n  colors: PropTypes.any.isRequired,\n};\n\nexport default PieChart;\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Templates/Swarm.js",
    "content": "/* eslint-disable react/prop-types */\n/* eslint-disable react/jsx-curly-newline */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ResponsiveSwarmPlot } from '@nivo/swarmplot';\n\nimport { theme } from './theme';\nimport { WrappedCard } from '../Organization';\n\nconst MemoizedResponsiveSwarmPlot = React.memo(\n  ResponsiveSwarmPlot,\n  (prevProps, nextProps) => prevProps.data?.length === nextProps.data?.length,\n);\n\nconst SwarmPlot = ({\n  header,\n  data,\n  groupBy,\n  groups,\n  legend,\n  formatXAxis,\n  formatYAxis,\n}) => {\n  const tickValues = [0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => 10800 * i);\n\n  return (\n    <div className=\"w-full h-96\">\n      <WrappedCard>\n        <p className=\"text-lg lg:text-xl font-semibold\">{header}</p>\n        <p>{`${data.length} Sampled Contributions, Eastern Time`}</p>\n        {Array.isArray(data) && data.length > 0 ? (\n          <MemoizedResponsiveSwarmPlot\n            theme={theme}\n            isInteractive={false}\n            animate={false}\n            data={data}\n            groupBy={groupBy}\n            groups={groups}\n            identity=\"id\"\n            value=\"timestamp\"\n            size={6}\n            forceStrength={4}\n            simulationIterations={60}\n            colors={{ scheme: 'category10' }}\n            gridYValues={tickValues}\n            valueScale={{ type: 'linear', min: 0, max: 86400, reverse: true }}\n            margin={{ top: 20, right: 0, bottom: 20, left: 70 }}\n            axisTop={null}\n            axisRight={null}\n            axisBottom={{\n              orient: 'bottom',\n              tickSize: 10,\n              tickPadding: 5,\n              tickRotation: 0,\n              legend,\n              legendPosition: 'middle',\n              legendOffset: 46,\n              format: (value) => formatXAxis(value),\n            }}\n            axisLeft={{\n              orient: 'left',\n              tickSize: 10,\n              tickPadding: 5,\n              tickRotation: 0,\n              legend: 'Time of Day',\n              legendPosition: 'middle',\n              legendOffset: -86,\n              tickValues,\n              format: (value) => formatYAxis(value),\n            }}\n          />\n        ) : (\n          <div className=\"w-full h-full flex items-center justify-center\">\n            No data to show\n          </div>\n        )}\n      </WrappedCard>\n    </div>\n  );\n};\n\nSwarmPlot.propTypes = {\n  header: PropTypes.string.isRequired,\n  data: PropTypes.array.isRequired,\n  groupBy: PropTypes.string.isRequired,\n  groups: PropTypes.array.isRequired,\n  legend: PropTypes.string.isRequired,\n  formatXAxis: PropTypes.func.isRequired,\n  formatYAxis: PropTypes.func.isRequired,\n};\n\nexport default SwarmPlot;\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Templates/index.js",
    "content": "import BarGraph from './Bar';\nimport PieChart from './Pie';\nimport SwarmPlot from './Swarm';\n\nexport * from './Numeric';\nexport * from './theme';\nexport { BarGraph, PieChart, SwarmPlot };\n"
  },
  {
    "path": "frontend/src/components/Wrapped/Templates/theme.js",
    "content": "export const theme = {\n  fontSize: '12px',\n  fontFamily: 'Segoe UI',\n};\n\nexport const scale = ['#EBEDF0', '#9BE9A8', '#40C463', '#30A14E', '#216E39'];\nexport const hoverScale = [\n  '#A6C9F5',\n  '#7EC7D1',\n  '#50B5AF',\n  '#48A3A4',\n  '#418A9A',\n];\nexport const singleHoverScale = [\n  '#468CBF',\n  '#468CBF',\n  '#468CBF',\n  '#468CBF',\n  '#468CBF',\n];\n"
  },
  {
    "path": "frontend/src/components/Wrapped/index.js",
    "content": "export * from './Organization';\nexport * from './Templates';\nexport * from './Specifics';\n"
  },
  {
    "path": "frontend/src/components/index.js",
    "content": "import Preview from './Preview';\n\nexport * from './Generic';\nexport * from './Card';\nexport * from './Home';\nexport * from './Wrapped';\n\nexport { Preview };\n"
  },
  {
    "path": "frontend/src/constants.js",
    "content": "/* eslint-disable no-nested-ternary */\nexport const PROD = process.env.REACT_APP_PROD === 'true';\n\nexport const USE_LOGGER = true;\n\nexport const CLIENT_ID = PROD\n  ? process.env.REACT_APP_PROD_CLIENT_ID\n  : process.env.REACT_APP_DEV_CLIENT_ID;\n\nexport const MODE = process.env.REACT_APP_MODE;\n\nexport const REDIRECT_URI = PROD\n  ? MODE === 'trends'\n    ? 'https://www.githubtrends.io/user'\n    : 'https://www.githubtrends.io/user/wrapped'\n  : MODE === 'trends'\n  ? 'http://localhost:3000/user'\n  : 'http://localhost:3000/user/wrapped';\n\nexport const GITHUB_PRIVATE_AUTH_URL = `https://github.com/login/oauth/authorize?scope=user,repo&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}/private`;\nexport const GITHUB_PUBLIC_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}/public`;\n\nexport const WRAPPED_URL = PROD\n  ? 'https://www.githubwrapped.io'\n  : 'http://localhost:3001';\n\nexport const BACKEND_URL = PROD\n  ? 'https://api.githubtrends.io'\n  : 'http://localhost:8000';\n\nexport const CURR_YEAR = 2024;\n"
  },
  {
    "path": "frontend/src/index.css",
    "content": "/* ./src/index.css */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n  margin: 0;\n  font-family: 'Segoe UI', Ubuntu, Sans-Serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n"
  },
  {
    "path": "frontend/src/index.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport { Provider } from 'react-redux';\n\nimport configureStore from './redux/store';\nimport { AppTrends, AppWrapped } from './pages/App';\nimport './index.css';\n\nimport { MODE } from './constants';\n\nexport const store = configureStore();\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\n\nif (MODE === 'trends') {\n  root.render(\n    <Provider store={store}>\n      <AppTrends />\n    </Provider>,\n  );\n} else if (MODE === 'wrapped') {\n  root.render(\n    <Provider store={store}>\n      <AppWrapped />\n    </Provider>,\n  );\n} else {\n  // Throw an error if the mode is not set correctly.\n  throw new Error(\n    'REACT_APP_MODE must be set to \"trends\" or \"wrapped\" in your .env file.',\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/App/AppTrends.js",
    "content": "/* eslint-disable jsx-a11y/anchor-is-valid */\n\nimport React, { useEffect } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\n\nimport {\n  BrowserRouter as Router,\n  Routes,\n  Route,\n  useParams,\n} from 'react-router-dom';\n\nimport Header from './Header';\nimport LandingScreen from '../Landing';\nimport DemoScreen from '../Demo';\nimport { SignUpScreen } from '../Auth';\nimport HomeScreen from '../Home';\nimport SettingsScreen from '../Settings';\nimport { NoMatchScreen, RedirectScreen } from '../Misc';\n\nimport { setPrivateAccess as _setPrivateAccess } from '../../redux/actions/userActions';\nimport { getUserMetadata } from '../../api';\nimport { WRAPPED_URL } from '../../constants';\nimport Footer from './Footer';\n\nfunction WrappedAuthRedirectScreen() {\n  // for wrapped auth redirects\n  const { rest } = useParams();\n  useEffect(() => {\n    const code = new URLSearchParams(window.location.search).get('code');\n    window.location.href = `${WRAPPED_URL}/${rest}?code=${code}`;\n  }, [rest]);\n\n  return null;\n}\n\nfunction WrappedRedirectScreen() {\n  // redirects /wrapped/* to https://www.githubwrapped.com/*\n  const { userId, year } = useParams();\n  useEffect(() => {\n    if (userId) {\n      if (year) {\n        window.location.href = `${WRAPPED_URL}/${userId}/${year}`;\n      } else {\n        window.location.href = `${WRAPPED_URL}/${userId}`;\n      }\n    } else {\n      window.location.href = `${WRAPPED_URL}/`;\n    }\n  }, [userId, year]);\n}\n\nfunction App() {\n  const userId = useSelector((state) => state.user.userId);\n  const isAuthenticated = userId && userId.length > 0;\n\n  const dispatch = useDispatch();\n  const setPrivateAccess = (access) => dispatch(_setPrivateAccess(access));\n\n  useEffect(() => {\n    async function getPrivateAccess() {\n      if (userId && userId.length > 0) {\n        const result = await getUserMetadata(userId);\n        if (result !== null && result.private_access !== undefined) {\n          setPrivateAccess(result.private_access);\n        }\n      }\n    }\n    getPrivateAccess();\n  }, [userId]);\n\n  return (\n    <div className=\"h-screen flex flex-col\">\n      <Router>\n        <Header mode=\"trends\" />\n        <section className=\"bg-white text-gray-700 flex-grow\">\n          <Routes>\n            {!isAuthenticated && (\n              <Route path=\"/signup\" element={<SignUpScreen />} />\n            )}\n            <Route path=\"/demo\" element={<DemoScreen />} />\n            <Route path=\"/user/redirect\" element={<RedirectScreen />} />\n            <Route\n              path=\"/user/wrapped/:rest\"\n              element={<WrappedAuthRedirectScreen />}\n            />\n            <Route path=\"/user/*\" element={<HomeScreen />} />\n            <Route\n              path=\"/wrapped/:userId/:year\"\n              element={<WrappedRedirectScreen />}\n            />\n            <Route\n              path=\"/wrapped/:userId\"\n              element={<WrappedRedirectScreen />}\n            />\n            <Route path=\"/wrapped\" element={<WrappedRedirectScreen />} />\n            <Route path=\"/settings\" element={<SettingsScreen />} />\n            <Route path=\"/:userId\" element={<WrappedRedirectScreen />} />\n            <Route exact path=\"/\" element={<LandingScreen />} />\n            <Route path=\"*\" element={<NoMatchScreen />} />\n          </Routes>\n        </section>\n        <Footer />\n      </Router>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "frontend/src/pages/App/AppWrapped.js",
    "content": "/* eslint-disable jsx-a11y/anchor-is-valid */\n\nimport React, { useEffect } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\n\nimport { BrowserRouter as Router, Routes, Route } from 'react-router-dom';\n\nimport Header from './Header';\nimport { SignUpScreen } from '../Auth';\nimport { SelectUserScreen, WrappedScreen } from '../Wrapped';\nimport { NoMatchScreen } from '../Misc';\n\nimport { setPrivateAccess as _setPrivateAccess } from '../../redux/actions/userActions';\nimport { getUserMetadata } from '../../api';\nimport Footer from './Footer';\n\nfunction App() {\n  const userId = useSelector((state) => state.user.userId);\n  const isAuthenticated = userId && userId.length > 0;\n\n  const dispatch = useDispatch();\n  const setPrivateAccess = (access) => dispatch(_setPrivateAccess(access));\n\n  useEffect(() => {\n    async function getPrivateAccess() {\n      if (userId && userId.length > 0) {\n        const result = await getUserMetadata(userId);\n        if (result !== null && result.private_access !== undefined) {\n          setPrivateAccess(result.private_access);\n        }\n      }\n    }\n    getPrivateAccess();\n  }, [userId]);\n\n  return (\n    <div className=\"h-screen flex flex-col\">\n      <Router>\n        <Header mode=\"wrapped\" />\n        <section className=\"bg-white text-gray-700 flex-grow\">\n          <Routes>\n            {!isAuthenticated && (\n              <Route path=\"/signup\" element={<SignUpScreen />} />\n            )}\n            <Route path=\"/\" element={<SelectUserScreen />} />\n            <Route path=\"/public/\" element={<SelectUserScreen />} />\n            <Route path=\"/private/\" element={<SelectUserScreen />} />\n            <Route path=\"/:userId/:year\" element={<WrappedScreen />} />\n            <Route path=\"/:userId\" element={<WrappedScreen />} />\n            <Route path=\"*\" element={<NoMatchScreen />} />\n          </Routes>\n        </section>\n        <Footer />\n      </Router>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "frontend/src/pages/App/Footer.js",
    "content": "import React from 'react';\nimport { CURR_YEAR } from '../../constants';\n\nfunction Footer() {\n  return (\n    <footer className=\"body-font\">\n      <div className=\"bg-gray-100 border-t border-gray-300\">\n        <div className=\"container mx-auto py-4 px-5\">\n          <p className=\"text-gray-500 text-sm text-center\">\n            {`© ${CURR_YEAR} GitHub Trends`}\n          </p>\n        </div>\n      </div>\n    </footer>\n  );\n}\n\nexport default Footer;\n"
  },
  {
    "path": "frontend/src/pages/App/Header.js",
    "content": "import React, { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport PropTypes from 'prop-types';\n\nimport { Link } from 'react-router-dom';\n\nimport { GiHamburgerMenu as HamburgerIcon } from 'react-icons/gi';\nimport { MdSettings as SettingsIcon } from 'react-icons/md';\n\nimport { logout as _logout } from '../../redux/actions/userActions';\nimport rocketIcon from '../../assets/rocket.png';\nimport { classnames } from '../../utils';\nimport { GITHUB_PUBLIC_AUTH_URL, WRAPPED_URL } from '../../constants';\n\nconst propTypes = {\n  to: PropTypes.string.isRequired,\n  children: PropTypes.node.isRequired,\n  onClick: PropTypes.func,\n  className: PropTypes.string,\n};\n\nconst defaultProps = {\n  onClick: null,\n  className: null,\n};\n\nconst StandardLink = ({ to, children, onClick, className }) => (\n  <Link\n    to={to}\n    className={classnames(\n      'px-4 py-1 mr-3 rounded-sm bg-gray-200 hover:bg-gray-300 text-gray-700',\n      className,\n    )}\n    onClick={onClick}\n  >\n    {children}\n  </Link>\n);\n\nStandardLink.propTypes = propTypes;\n\nStandardLink.defaultProps = defaultProps;\n\nconst MobileLink = ({ to, children, onClick, className }) => (\n  <Link\n    to={to}\n    className={classnames(\n      'block text-sm px-2 my-2 py-2 rounded-sm bg-gray-200 text-gray-700',\n      className,\n    )}\n    onClick={onClick}\n  >\n    {children}\n  </Link>\n);\n\nMobileLink.propTypes = propTypes;\n\nMobileLink.defaultProps = defaultProps;\n\nconst Header = ({ mode }) => {\n  const [toggle, setToggle] = useState(false);\n\n  const userId = useSelector((state) => state.user.userId);\n  const isAuthenticated = userId && userId.length > 0;\n\n  const dispatch = useDispatch();\n  const logout = () => dispatch(_logout());\n\n  return (\n    <div className=\"text-gray-100 bg-white shadow-md body-font top-0 z-50\">\n      <div className=\"p-5 flex flex-wrap\">\n        {/* GitHub Trends Logo */}\n        <Link\n          to=\"/\"\n          className=\"flex items-center title-font font-medium text-gray-700 mb-0 md:mr-8\"\n        >\n          <img src={rocketIcon} alt=\"logo\" className=\"w-6 h-6\" />\n          {mode === 'trends' && (\n            <span className=\"ml-2 text-xl\">GitHub Trends</span>\n          )}\n          {mode === 'wrapped' && (\n            <span className=\"ml-2 text-xl\">GitHub Wrapped</span>\n          )}\n        </Link>\n        {/* Pages: Wrapped, Dashboard, Demo */}\n        {mode === 'trends' && (\n          <div className=\"hidden md:flex\">\n            <Link\n              to={WRAPPED_URL}\n              className=\"px-4 py-1 mr-3 rounded-sm bg-blue-500 hover:bg-blue-600 text-white\"\n            >\n              Wrapped\n            </Link>\n            {isAuthenticated ? (\n              <StandardLink to=\"/user\">Dashboard</StandardLink>\n            ) : (\n              <StandardLink to=\"/demo\">Demo</StandardLink>\n            )}\n          </div>\n        )}\n        {/* Auth Pages: Sign Up, Log In, Log Out */}\n        <div className=\"hidden md:flex ml-auto items-center text-base justify-center\">\n          {isAuthenticated ? (\n            <>\n              {mode === 'trends' && (\n                <Link to=\"/settings\" className=\"mr-3 px-1 py-1\">\n                  <SettingsIcon className=\"h-6 w-6 text-gray-700\" />\n                </Link>\n              )}\n              <StandardLink to=\"/\" onClick={logout}>\n                Sign Out\n              </StandardLink>\n            </>\n          ) : (\n            <>\n              <a\n                className=\"px-4 py-1 mr-3 rounded-sm bg-gray-200 hover:bg-gray-300 text-gray-700\"\n                href={GITHUB_PUBLIC_AUTH_URL}\n              >\n                Login\n              </a>\n              <Link\n                to=\"/signup\"\n                className=\"px-4 py-1 mr-3 rounded-sm bg-blue-500 hover:bg-blue-700 hover:text-gray-300\"\n              >\n                Sign Up\n              </Link>\n            </>\n          )}\n        </div>\n        {/* Hamburger Menu */}\n        <div className=\"md:hidden flex ml-auto items-center\">\n          <button\n            type=\"button\"\n            className=\"outline-none\"\n            onClick={() => setToggle(!toggle)}\n          >\n            <HamburgerIcon className=\"w-6 h-6 text-gray-700\" />\n          </button>\n        </div>\n      </div>\n      {/* Hamburger Dropdown */}\n      <div className={classnames('p-5 pt-0', !toggle && 'hidden')}>\n        {mode === 'trends' && (\n          <>\n            <MobileLink to={WRAPPED_URL} onClick={() => setToggle(false)}>\n              Wrapped\n            </MobileLink>\n            {isAuthenticated ? (\n              <MobileLink to=\"/user\" onClick={() => setToggle(false)}>\n                Dashboard\n              </MobileLink>\n            ) : (\n              <MobileLink to=\"/demo\" onClick={() => setToggle(false)}>\n                Demo\n              </MobileLink>\n            )}\n          </>\n        )}\n        {isAuthenticated ? (\n          <>\n            {mode === 'trends' && (\n              <MobileLink to=\"/settings\" onClick={() => setToggle(false)}>\n                Settings\n              </MobileLink>\n            )}\n            <MobileLink\n              to=\"/\"\n              onClick={() => {\n                setToggle(false);\n                logout();\n              }}\n            >\n              Sign Out\n            </MobileLink>\n          </>\n        ) : (\n          <>\n            <a\n              className=\"block text-sm px-2 my-2 py-2 rounded-sm bg-gray-200 text-gray-700\"\n              href={GITHUB_PUBLIC_AUTH_URL}\n            >\n              Login\n            </a>\n            <Link\n              to=\"/signup\"\n              onClick={() => setToggle(false)}\n              className=\"block text-sm px-2 my-2 py-2 rounded-sm bg-blue-500 text-white\"\n            >\n              Sign Up\n            </Link>\n          </>\n        )}\n      </div>\n    </div>\n  );\n};\n\nHeader.propTypes = {\n  mode: PropTypes.string.isRequired,\n};\n\nexport default Header;\n"
  },
  {
    "path": "frontend/src/pages/App/index.js",
    "content": "import AppTrends from './AppTrends';\nimport AppWrapped from './AppWrapped';\n\nexport { AppTrends, AppWrapped };\n"
  },
  {
    "path": "frontend/src/pages/Auth/SignUp.js",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\n\nimport { FaGithub as GithubIcon } from 'react-icons/fa';\n\nimport { Button } from '../../components';\nimport {\n  GITHUB_PUBLIC_AUTH_URL,\n  GITHUB_PRIVATE_AUTH_URL,\n} from '../../constants';\nimport { classnames } from '../../utils';\n\nimport mockup from '../../assets/mockup.png';\n\nconst SignUpScreen = () => {\n  // eslint-disable-next-line no-unused-vars\n  const userId = useSelector((state) => state.user.userId);\n\n  return (\n    <div className=\"h-full flex flex-wrap\">\n      <div className=\"hidden lg:block lg:w-3/5 lg:p-8 lg:my-auto\">\n        <div className=\"w-4/5 mx-auto\">\n          <img src={mockup} alt=\"preview\" />\n        </div>\n      </div>\n      <div className=\"w-full h-full lg:w-2/5 flex lg:flex-col lg:p-8\">\n        <div\n          className={classnames(\n            'bg-gray-100 rounded-sm w-full h-full m-auto p-8 shadow',\n            'lg:w-80 lg:h-auto',\n          )}\n        >\n          <h3 className=\"text-3xl font-semibold text-gray-900 mb-12 text-center\">\n            Sign up for <br />\n            GitHub Trends\n          </h3>\n          <div>\n            <a href={GITHUB_PUBLIC_AUTH_URL} className=\"flex justify-center\">\n              <Button className=\"h-12 flex justify-center items-center text-white bg-blue-500 hover:bg-blue-600\">\n                <GithubIcon className=\"w-4 h-4\" />\n                <span className=\"ml-2 xl:text-lg\">GitHub Public Access</span>\n              </Button>\n            </a>\n            <div className=\"mt-4\" />\n            <a href={GITHUB_PRIVATE_AUTH_URL} className=\"flex justify-center\">\n              <Button className=\"h-12 flex justify-center items-center text-black border border-black bg-white hover:bg-gray-100\">\n                <GithubIcon className=\"w-4 h-4\" />\n                <span className=\"ml-2 xl:text-lg\">GitHub Private Access</span>\n              </Button>\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default SignUpScreen;\n"
  },
  {
    "path": "frontend/src/pages/Auth/index.js",
    "content": "import SignUpScreen from './SignUp';\n\nexport { SignUpScreen };\n"
  },
  {
    "path": "frontend/src/pages/Demo/Demo.js",
    "content": "import React, { useEffect, useState } from 'react';\n\nimport { Link } from 'react-router-dom';\n\nimport { Button, SvgInline } from '../../components';\nimport { getValidUser } from '../../api/wrapped';\nimport { BACKEND_URL } from '../../constants';\nimport { classnames } from '../../utils';\n\nconst DemoScreen = () => {\n  const [userName, setUserName] = useState('');\n  const [selectedUserName, setSelectedUserName] = useState('');\n  const [loading, setLoading] = useState(false);\n\n  let userNameInput;\n\n  useEffect(() => {\n    userNameInput.focus();\n  }, [userNameInput]);\n\n  const [error, setError] = useState('');\n\n  const handleSubmit = async () => {\n    const validUser = await getValidUser(userName);\n    if (validUser.includes('Valid user') || validUser === 'Repo not starred') {\n      setLoading(true);\n      setSelectedUserName(userName);\n      setLoading(false);\n    } else if (validUser === 'GitHub user not found') {\n      setError('GitHub user not found. Check your spelling and try again.');\n    }\n  };\n\n  const firstCardUrl =\n    selectedUserName.length > 0\n      ? `${BACKEND_URL}/user/svg/${selectedUserName}/langs?demo=true`\n      : `${BACKEND_URL}/user/svg/demo?card=langs`;\n\n  const secondCardUrl =\n    selectedUserName.length > 0\n      ? `${BACKEND_URL}/user/svg/${selectedUserName}/repos?demo=true`\n      : `${BACKEND_URL}/user/svg/demo?card=repos`;\n\n  return (\n    <div className=\"h-full py-8 flex flex-col xl:flex-row justify-center items-center\">\n      <div className=\"h-full w-full px-5 pb-5 xl:w-1/3 xl:pl-8 xl:pr-0 xl:pb-0\">\n        <div className=\"h-full w-full bg-gray-100 rounded-sm p-4 shadow\">\n          <h1 className=\"text-2xl font-bold text-gray-800 text-center mb-4\">\n            GitHub Trends Demo\n          </h1>\n          <p className=\"text-center text-sm text-gray-600\">\n            This is a demo of the GitHub Trends API. Enter your GitHub username\n            to see statistics about your top languages and repositories from the\n            past month.\n          </p>\n          <div className=\"form-control my-8\">\n            <p>Enter your GitHub username to get started!</p>\n            <div className=\"flex space-x-2 mt-2\">\n              <input\n                type=\"text\"\n                ref={(input) => {\n                  userNameInput = input;\n                }}\n                placeholder=\"Enter Username\"\n                className={classnames(\n                  'bg-white text-gray-700 w-full input input-bordered rounded-sm',\n                  error && 'input-error',\n                )}\n                onChange={(e) => {\n                  setUserName(e.target.value);\n                  setError('');\n                }}\n                onKeyPress={async (e) => {\n                  if (e.key === 'Enter') {\n                    handleSubmit();\n                  }\n                }}\n              />\n              <Button\n                type=\"submit\"\n                className=\"bg-blue-500 hover:bg-blue-700 text-white\"\n                onClick={handleSubmit}\n              >\n                Go\n              </Button>\n            </div>\n            {error ? (\n              <div className=\"text-red-500 text-sm mt-2\">\n                <strong>Error:</strong> {error}\n              </div>\n            ) : (\n              <div className=\"text-sm mt-2 py-5\" />\n            )}\n          </div>\n          <p className=\"text-center text-sm text-red-500\">\n            This demo uses a public access token that is heavily rate limited.\n            For full customization, private contributions, and a personal access\n            token, create an account instead!\n          </p>\n          <div className=\"flex justify-center mt-8\">\n            <Button className=\"text-white bg-blue-500 hover:bg-blue-600\">\n              <Link to=\"/signup\">Create an Account</Link>\n            </Button>\n          </div>\n        </div>\n      </div>\n      <div className=\"h-full w-full xl:w-2/3 px-5 xl:px-8\">\n        <div className=\"h-full w-full bg-gray-100 rounded-sm p-4 shadow\">\n          <h1 className=\"text-2xl font-bold text-gray-800 text-center mb-4\">\n            {selectedUserName === ''\n              ? 'Enter a Username'\n              : `Example Cards for ${selectedUserName}`}\n          </h1>\n          <div className=\"w-full flex flex-wrap\">\n            <div className=\"w-full lg:w-1/2 p-2\">\n              <SvgInline\n                className=\"w-full h-full\"\n                url={firstCardUrl}\n                forceLoading={loading}\n              />\n            </div>\n            <div className=\"w-full lg:w-1/2 p-2\">\n              <SvgInline\n                className=\"w-full h-full\"\n                url={secondCardUrl}\n                forceLoading={loading}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default DemoScreen;\n"
  },
  {
    "path": "frontend/src/pages/Demo/index.js",
    "content": "import DemoScreen from './Demo';\n\nexport default DemoScreen;\n"
  },
  {
    "path": "frontend/src/pages/Home/Home.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport { useNavigate } from 'react-router-dom';\nimport BounceLoader from 'react-spinners/BounceLoader';\nimport { FaGithub as GithubIcon } from 'react-icons/fa';\n\nimport { ProgressBar } from '../../components';\nimport {\n  SelectCardStage,\n  CustomizeStage,\n  ThemeStage,\n  DisplayStage,\n} from './stages';\n\nimport { setUserKey, authenticate } from '../../api';\nimport { login as _login } from '../../redux/actions/userActions';\nimport { PROD } from '../../constants';\n\nconst HomeScreen = () => {\n  const navigate = useNavigate();\n\n  const [isLoading, setIsLoading] = useState(false);\n\n  const userId = useSelector((state) => state.user.userId);\n  const privateAccess = useSelector((state) => state.user.privateAccess);\n\n  const isAuthenticated = userId && userId.length > 0;\n\n  const dispatch = useDispatch();\n\n  const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey));\n\n  // for all stages\n  const [stage, setStage] = useState(0);\n\n  // for stage one\n  const [selectedCard, setSelectedCard] = useState('langs');\n\n  // for stage two\n  const defaultTimeRange = {\n    id: 3,\n    label: 'Past 1 Year',\n    disabled: false,\n    value: 'one_year',\n  };\n  const [selectedTimeRange, setSelectedTimeRange] = useState(defaultTimeRange);\n\n  const [usePercent, setUsePercent] = useState(false);\n  const [usePrivate, setUsePrivate] = useState(false);\n  const [groupOther, setGroupOther] = useState(false);\n  const [groupPrivate, setGroupPrivate] = useState(false);\n  const [useLocChanged, setUseLocChanged] = useState(false);\n  const [useCompact, setUseCompact] = useState(false);\n\n  const resetCustomization = () => {\n    setSelectedTimeRange(defaultTimeRange);\n    setUsePercent(false);\n    setUsePrivate(false);\n    setUseLocChanged(false);\n    setUseCompact(false);\n  };\n\n  useEffect(() => {\n    resetCustomization();\n  }, [selectedCard]);\n\n  const time = selectedTimeRange.value;\n  let fullSuffix = `${selectedCard}?time_range=${time}`;\n\n  if (usePercent) {\n    fullSuffix += '&use_percent=True';\n  }\n\n  if (usePrivate) {\n    fullSuffix += '&include_private=True';\n  }\n\n  if (usePrivate && groupOther && groupPrivate) {\n    fullSuffix += '&group=private';\n  } else if (groupOther) {\n    fullSuffix += '&group=other';\n  }\n\n  if (useLocChanged) {\n    fullSuffix += '&loc_metric=changed';\n  }\n\n  if (useCompact) {\n    fullSuffix += '&compact=True';\n  }\n\n  // for stage three\n  const [theme, setTheme] = useState('classic');\n  const themeSuffix = `${fullSuffix}&theme=${theme}`;\n\n  useEffect(() => {\n    async function redirectCode() {\n      // After requesting Github access, Github redirects back to your app with a code parameter\n      const url = window.location.href;\n\n      if (url.includes('error=')) {\n        navigate('/');\n      }\n\n      // If Github API returns the code parameter\n      if (url.includes('code=')) {\n        const tempPrivateAccess = url.includes('private');\n        const newUrl = url.split('?code=');\n        const subStr = PROD ? 'githubtrends.io' : 'localhost:3000';\n        const redirect = `${url.split(subStr)[0]}${subStr}/user`;\n        window.history.pushState({}, null, redirect);\n        setIsLoading(true);\n        const userKey = await setUserKey(newUrl[1]);\n        const newUserId = await authenticate(newUrl[1], tempPrivateAccess);\n        login(newUserId, userKey);\n        setIsLoading(false);\n      }\n    }\n\n    redirectCode();\n  }, []);\n\n  if (isLoading) {\n    return (\n      <div className=\"h-full py-8 flex justify-center items-center\">\n        <BounceLoader color=\"#3B82F6\" />\n      </div>\n    );\n  }\n\n  if (!isAuthenticated) {\n    return (\n      <div className=\"h-full py-8 flex justify-center items-center\">\n        <div className=\"text-center\">\n          <h1 className=\"text-3xl font-bold\">\n            Please sign in to access this page\n          </h1>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"h-full px-2 py-4 lg:p-8 text-gray-600 body-font\">\n      <div className=\"flex flex-col\">\n        <ProgressBar\n          items={[\n            'Select Card',\n            'Modify Parameters',\n            'Customize Theme',\n            'Display Card',\n          ]}\n          currItem={stage}\n          setCurrItem={setStage}\n        />\n        <div className=\"m-4 rounded-sm\">\n          <div className=\"lg:p-4\">\n            <div className=\"text-2xl text-gray-600 font-semibold\">\n              {\n                [\n                  'Select a Card',\n                  'Modify Card Parameters',\n                  'Choose a Theme',\n                  'Display your Card',\n                ][stage]\n              }\n            </div>\n            <div>\n              {\n                [\n                  'You will be able to customize your card in future steps.',\n                  'Change the date range, include private commits, and more!',\n                  'Choose from one of our predefined themes (more coming soon!)',\n                  'Display your card on GitHub, Twitter, or Linkedin',\n                ][stage]\n              }\n            </div>\n          </div>\n          {stage === 0 && (\n            <SelectCardStage\n              selectedCard={selectedCard}\n              setSelectedCard={setSelectedCard}\n            />\n          )}\n          {stage === 1 && (\n            <CustomizeStage\n              selectedCard={selectedCard}\n              selectedTimeRange={selectedTimeRange}\n              setSelectedTimeRange={setSelectedTimeRange}\n              usePrivate={usePrivate}\n              setUsePrivate={setUsePrivate}\n              groupOther={groupOther}\n              setGroupOther={setGroupOther}\n              groupPrivate={groupPrivate}\n              setGroupPrivate={setGroupPrivate}\n              privateAccess={privateAccess}\n              useCompact={useCompact}\n              setUseCompact={setUseCompact}\n              usePercent={usePercent}\n              setUsePercent={setUsePercent}\n              useLocChanged={useLocChanged}\n              setUseLocChanged={setUseLocChanged}\n              fullSuffix={fullSuffix}\n            />\n          )}\n          {stage === 2 && (\n            <ThemeStage\n              theme={theme}\n              setTheme={setTheme}\n              fullSuffix={fullSuffix}\n            />\n          )}\n          {stage === 3 && (\n            <DisplayStage userId={userId} themeSuffix={themeSuffix} />\n          )}\n        </div>\n      </div>\n      <div className=\"fixed bottom-8 right-8\">\n        <a\n          href=\"https://www.github.com/avgupta456/github-trends\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <button\n            type=\"button\"\n            className=\"rounded-sm shadow bg-gray-700 hover:bg-gray-800 text-gray-50 px-3 py-2 flex items-center\"\n          >\n            Star on\n            <GithubIcon className=\"ml-1.5 w-5 h-5\" />\n          </button>\n        </a>\n      </div>\n    </div>\n  );\n};\n\nexport default HomeScreen;\n"
  },
  {
    "path": "frontend/src/pages/Home/index.js",
    "content": "import HomeScreen from './Home';\n\nexport default HomeScreen;\n"
  },
  {
    "path": "frontend/src/pages/Home/stages/Customize.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Image, DateRangeSection, CheckboxSection } from '../../../components';\n\nconst CustomizeStage = ({\n  selectedCard,\n  selectedTimeRange,\n  setSelectedTimeRange,\n  usePrivate,\n  setUsePrivate,\n  groupOther,\n  setGroupOther,\n  groupPrivate,\n  setGroupPrivate,\n  privateAccess,\n  useCompact,\n  setUseCompact,\n  usePercent,\n  setUsePercent,\n  useLocChanged,\n  setUseLocChanged,\n  fullSuffix,\n}) => {\n  return (\n    <div className=\"w-full flex flex-wrap\">\n      <div className=\"h-auto lg:w-2/5 md:w-1/2 pr-10 p-10 rounded-sm bg-gray-200\">\n        <DateRangeSection\n          selectedTimeRange={selectedTimeRange}\n          setSelectedTimeRange={setSelectedTimeRange}\n          privateAccess={privateAccess}\n        />\n        {selectedCard === 'langs' && (\n          <CheckboxSection\n            title=\"Compact View\"\n            text=\"Use default view or compact view.\"\n            question=\"Use compact view?\"\n            variable={useCompact}\n            setVariable={setUseCompact}\n          />\n        )}\n        <CheckboxSection\n          title=\"Include Private Repositories?\"\n          text=\"By default, private commits are hidden. We will never reveal private repository information.\"\n          question=\"Use private commits?\"\n          variable={usePrivate}\n          setVariable={setUsePrivate}\n          disabled={!privateAccess}\n        />\n        {selectedCard === 'repos' && (\n          <CheckboxSection\n            title=\"Group Other Repositories?\"\n            text=\"Group all remaining repositories together at the bottom of the card.\"\n            question=\"Group other repositories?\"\n            variable={groupOther}\n            setVariable={setGroupOther}\n          />\n        )}\n        {selectedCard === 'repos' && usePrivate && groupOther && (\n          <CheckboxSection\n            title=\"Group Private Repositories?\"\n            text=\"Force private repositories together at the bottom of the card.\"\n            question=\"Group private commits?\"\n            variable={groupPrivate}\n            setVariable={setGroupPrivate}\n          />\n        )}\n        {selectedCard === 'langs' && (\n          <CheckboxSection\n            title=\"Percent vs LOC\"\n            text=\"Use absolute LOC (default) or percent to rank your top repositories\"\n            question=\"Use percent?\"\n            variable={usePercent}\n            setVariable={setUsePercent}\n            disabled={useCompact}\n          />\n        )}\n        <CheckboxSection\n          title=\"LOC Metric\"\n          text=\"By default, LOC are measured as Added: (+) - (-). Alternatively, you can use Changed: (+) + (-)\"\n          question=\"Use LOC changed?\"\n          variable={useLocChanged}\n          setVariable={setUseLocChanged}\n          disabled={selectedCard === 'langs' && usePercent}\n        />\n      </div>\n      <div className=\"w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0\">\n        <div className=\"w-full lg:w-3/5 mx-auto h-full flex flex-col justify-center\">\n          <Image imageSrc={fullSuffix} compact={useCompact} />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nCustomizeStage.propTypes = {\n  selectedCard: PropTypes.string.isRequired,\n  selectedTimeRange: PropTypes.object.isRequired,\n  setSelectedTimeRange: PropTypes.func.isRequired,\n  usePrivate: PropTypes.bool.isRequired,\n  setUsePrivate: PropTypes.func.isRequired,\n  groupOther: PropTypes.bool.isRequired,\n  setGroupOther: PropTypes.func.isRequired,\n  groupPrivate: PropTypes.bool.isRequired,\n  setGroupPrivate: PropTypes.func.isRequired,\n  privateAccess: PropTypes.bool.isRequired,\n  useCompact: PropTypes.bool.isRequired,\n  setUseCompact: PropTypes.func.isRequired,\n  usePercent: PropTypes.bool.isRequired,\n  setUsePercent: PropTypes.func.isRequired,\n  useLocChanged: PropTypes.bool.isRequired,\n  setUseLocChanged: PropTypes.func.isRequired,\n  fullSuffix: PropTypes.string.isRequired,\n};\n\nexport default CustomizeStage;\n"
  },
  {
    "path": "frontend/src/pages/Home/stages/Display.js",
    "content": "/* eslint-disable react/no-array-index-key */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { ToastContainer, toast } from 'react-toastify';\nimport 'react-toastify/dist/ReactToastify.css';\n\nimport { saveSvgAsPng } from 'save-svg-as-png';\n\nimport { Card, Button } from '../../../components';\nimport { classnames } from '../../../utils';\n\nconst DisplayStage = ({ userId, themeSuffix }) => {\n  const card = themeSuffix.split('?')[0];\n\n  const downloadPNG = () => {\n    saveSvgAsPng(document.getElementById('svg-card'), `${userId}_${card}.png`, {\n      scale: 2,\n      encoderOptions: 1,\n    });\n  };\n\n  const copyUrl = () => {\n    navigator.clipboard.writeText(\n      `https://api.githubtrends.io/user/svg/${userId}/${themeSuffix}`,\n    );\n    toast.info('Copied to Clipboard!', {\n      position: 'bottom-right',\n      autoClose: 1000,\n      hideProgressBar: true,\n      closeOnClick: false,\n      pauseOnHover: false,\n      draggable: false,\n      progress: undefined,\n    });\n  };\n\n  return (\n    <div className=\"w-full flex flex-wrap\">\n      <ToastContainer />\n      <div className=\"h-auto lg:w-2/5 md:w-1/2 pr-10 p-10 rounded-sm bg-gray-200\">\n        <div>\n          Copy the image URL or download the PNG. Share on GitHub, Twitter,\n          LinkedIn, or anywhere else!\n        </div>\n        <br />\n        <div className=\"flex flex-col items-center\">\n          {[\n            { title: 'Copy URL', active: true, onClick: copyUrl },\n            { title: 'Download PNG', active: true, onClick: downloadPNG },\n          ].map((item, index) => (\n            <Button\n              key={index}\n              className={classnames(\n                'm-4 w-60 flex justify-center',\n                item.active\n                  ? 'bg-blue-500 text-white'\n                  : 'bg-gray-300 text-gray-500 cursor-not-allowed',\n              )}\n              onClick={item.onClick}\n            >\n              {item.title}\n            </Button>\n          ))}\n        </div>\n      </div>\n      <div className=\"w-full lg:w-3/5 md:w-1/2 object-center pt-5 md:pt-0 pl-0 md:pl-5 lg:pl-0\">\n        <div className=\"w-full lg:w-3/5 mx-auto h-full flex flex-col justify-center\">\n          <Card\n            title=\"Your Card\"\n            description=\"The finished product!\"\n            imageSrc={`${themeSuffix}&use_animation=False`}\n            selected\n          />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nDisplayStage.propTypes = {\n  userId: PropTypes.string.isRequired,\n  themeSuffix: PropTypes.string.isRequired,\n};\n\nexport default DisplayStage;\n"
  },
  {
    "path": "frontend/src/pages/Home/stages/SelectCard.js",
    "content": "/* eslint-disable react/no-array-index-key */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Card } from '../../../components';\n\nconst SelectCardStage = ({ selectedCard, setSelectedCard }) => {\n  return (\n    <div className=\"w-full flex flex-wrap\">\n      {[\n        {\n          title: 'Language Contributions',\n          description: 'See your overall language breakdown',\n          imageSrc: 'langs',\n        },\n        {\n          title: 'Repository Contributions',\n          description: 'See your most contributed repositories',\n          imageSrc: 'repos',\n        },\n      ].map((card, index) => (\n        <button\n          className=\"w-full sm:w-1/2 lg:w-1/3 p-2 lg:p-4\"\n          key={index}\n          type=\"button\"\n          onClick={() => setSelectedCard(card.imageSrc)}\n        >\n          <Card\n            title={card.title}\n            description={card.description}\n            imageSrc={card.imageSrc}\n            selected={selectedCard === card.imageSrc}\n          />\n        </button>\n      ))}\n    </div>\n  );\n};\n\nSelectCardStage.propTypes = {\n  selectedCard: PropTypes.string.isRequired,\n  setSelectedCard: PropTypes.func.isRequired,\n};\n\nexport default SelectCardStage;\n"
  },
  {
    "path": "frontend/src/pages/Home/stages/Theme.js",
    "content": "/* eslint-disable react/no-array-index-key */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Card } from '../../../components';\n\nconst ThemeStage = ({ theme, setTheme, fullSuffix }) => {\n  return (\n    <div className=\"flex flex-wrap\">\n      {[\n        {\n          title: 'Classic',\n          imageSrc: 'classic',\n        },\n        {\n          title: 'Dark',\n          imageSrc: 'dark',\n        },\n        {\n          title: 'Bright Lights',\n          imageSrc: 'bright_lights',\n        },\n        {\n          title: 'Rosettes',\n          imageSrc: 'rosettes',\n        },\n        {\n          title: 'Ferns',\n          imageSrc: 'ferns',\n        },\n        {\n          title: 'Synthwaves',\n          imageSrc: 'synthwaves',\n        },\n      ].map((card, index) => (\n        <button\n          className=\"w-full sm:w-1/2 lg:w-1/3 p-2 lg:p-4\"\n          key={index}\n          type=\"button\"\n          onClick={() => setTheme(card.imageSrc)}\n        >\n          <Card\n            title={card.title}\n            description=\"\"\n            imageSrc={`${fullSuffix}&theme=${card.imageSrc}`}\n            selected={theme === card.imageSrc}\n          />\n        </button>\n      ))}\n    </div>\n  );\n};\n\nThemeStage.propTypes = {\n  theme: PropTypes.string.isRequired,\n  setTheme: PropTypes.func.isRequired,\n  fullSuffix: PropTypes.string.isRequired,\n};\n\nexport default ThemeStage;\n"
  },
  {
    "path": "frontend/src/pages/Home/stages/index.js",
    "content": "import SelectCardStage from './SelectCard';\nimport CustomizeStage from './Customize';\nimport ThemeStage from './Theme';\nimport DisplayStage from './Display';\n\nexport { SelectCardStage, CustomizeStage, ThemeStage, DisplayStage };\n"
  },
  {
    "path": "frontend/src/pages/Landing/Landing.js",
    "content": "/* eslint-disable react/jsx-one-expression-per-line */\n\nimport React from 'react';\nimport { useSelector } from 'react-redux';\n\nimport { Link } from 'react-router-dom';\nimport { FaGithub as GithubIcon, FaCheck as CheckIcon } from 'react-icons/fa';\n\nimport { Button, Preview } from '../../components';\n\nimport mockup from '../../assets/mockup.png';\nimport wrapped from '../../assets/wrapped1.png';\nimport avgupta456Langs from '../../assets/avgupta456_langs.png';\nimport tiangoloRepos from '../../assets/tiangolo_repos.png';\nimport reininkRepos from '../../assets/reinink_repos.png';\nimport dhermesLangs from '../../assets/dhermes_langs.png';\nimport { WRAPPED_URL } from '../../constants';\n\nfunction LandingScreen() {\n  const userId = useSelector((state) => state.user.userId);\n\n  const isAuthenticated = userId && userId.length > 0;\n\n  return (\n    <section>\n      <div className=\"min-h-screen bg-blue-500 text-gray-100 w-full flex flex-wrap -mt-8 py-16 px-4\">\n        <div className=\"w-full xl:w-1/2 flex flex-col justify-center p-4\">\n          <div className=\"w-full 3xl:w-2/3 mx-auto text-center text-3xl md:text-5xl 3xl:text-6xl font-bold mb-6\">\n            Discover and share code contribution insights\n          </div>\n          <div className=\"w-full 3xl:w-2/3 mx-auto text-center md:text-lg 3xl:text-2xl mb-2\">\n            GitHub Trends dives deep into the GitHub API to bring you insightful\n            metrics on your contributions, broken by repository and language.\n          </div>\n          <div className=\"w-full flex flex-wrap justify-center\">\n            <Link to={isAuthenticated ? '/user' : '/signup'} className=\"w-auto\">\n              <Button className=\"my-4 mr-4 w-auto justify-center bg-gray-700 hover:bg-gray-800 text-xl 3xl:text-2xl\">\n                {isAuthenticated ? 'Visit Dashboard' : 'Get Started'}\n              </Button>\n            </Link>\n            <a\n              href=\"https://www.github.com/avgupta456/github-trends\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"w-auto\"\n            >\n              <Button className=\"my-4 w-auto flex justify-center items-center bg-gray-100 hover:bg-gray-200 text-gray-700 text-xl 3xl:text-2xl\">\n                Star on\n                <GithubIcon className=\"ml-2 w-6 h-6\" />\n              </Button>\n            </a>\n          </div>\n        </div>\n        <div className=\"hidden md:flex w-3/4 xl:w-1/2 mx-auto items-center p-4\">\n          <img src={mockup} alt=\"preview\" />\n        </div>\n      </div>\n      <div className=\"bg-gray-200 text-gray-700 w-full flex flex-wrap items-center py-4 px-4\">\n        <div className=\"w-full lg:w-1/2 p-8 flex justify-center\">\n          <Preview\n            pages={[avgupta456Langs, tiangoloRepos, reininkRepos, dhermesLangs]}\n            details={[\n              'Abhijit Gupta (avgupta456): GitHub Trends',\n              'Sebastián Ramírez (tiangolo): FastAPI',\n              'Jonathan Reinink (reinink): TailwindCSS',\n              'Danny Hermes (dhermes): Google PubSub',\n            ]}\n            showArrows\n          />\n        </div>\n        <div className=\"w-full lg:w-1/2 3xl:w-1/3 mx-auto p-8 flex flex-col\">\n          <h1 className=\"text-4xl text-gray-900 font-medium mb-12\">\n            Display your GitHub stats\n            <div>with embeddable cards</div>\n          </h1>\n          <p className=\"text-lg font-bold\">1. Comprehensive</p>\n          <p>\n            GitHub Trends counts each individual commit, across all your\n            open-source contributions. We surface line of code metrics by\n            repository and languages.\n          </p>\n          <br />\n          <p className=\"text-lg font-bold\">2. Customizable</p>\n          <p>\n            Using the online dashboard, easily modify the time range, include\n            private commits, and choose your display theme.\n          </p>\n          <br />\n          <p className=\"text-lg font-bold\">3. Shareable</p>\n          <p>\n            Share your GitHub Trends cards as a PNG on Twitter, or as a dynamic\n            embeddable image on your GitHub profile or personal website.\n          </p>\n          <br />\n          <div>\n            <Link to={isAuthenticated ? '/user' : '/demo'} className=\"w-auto\">\n              <Button className=\"my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-gray-700 hover:bg-gray-800\">\n                {isAuthenticated ? 'Visit Dashboard' : 'Try the Demo'}\n              </Button>\n            </Link>\n            {!isAuthenticated && (\n              <Link to=\"/signup\" className=\"w-auto\">\n                <Button className=\"my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-blue-500 hover:bg-blue-600\">\n                  Get Started\n                </Button>\n              </Link>\n            )}\n          </div>\n        </div>\n      </div>\n      <div className=\"text-gray-700 w-full flex flex-wrap items-center py-4 px-4\">\n        <div className=\"w-full lg:w-1/2 3xl:w-1/3 mx-auto p-8 flex flex-col\">\n          <h1 className=\"text-4xl text-gray-900 font-medium mb-12\">\n            Reflect on your year\n            <div>\n              with <strong>GitHub Wrapped</strong>\n            </div>\n          </h1>\n          <p className=\"text-lg font-bold\">1. Detailed</p>\n          <p>\n            GitHub Wrapped provides a breakdown of your contributions by date,\n            by date, time, repository, and language. Over 20 stats are\n            displayed.\n          </p>\n          <br />\n          <p className=\"text-lg font-bold\">2. Visual</p>\n          <p>\n            Understand your coding contributions like never before with an\n            interactive calendar, bar charts, pie charts, and more.\n          </p>\n          <br />\n          <p className=\"text-lg font-bold\">3. Public</p>\n          <p>\n            Share your GitHub Wrapped link with your friends and colleagues and\n            take a look at their contributions too.{' '}\n            <strong>No account required</strong>, although rate limiting may\n            apply.\n          </p>\n          <br />\n          <div>\n            <Link to={`${WRAPPED_URL}/avgupta456`} className=\"w-auto\">\n              <Button className=\"my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-gray-700 hover:bg-gray-800\">\n                Example\n              </Button>\n            </Link>\n            <Link\n              to={isAuthenticated ? `${WRAPPED_URL}/${userId}` : WRAPPED_URL}\n              className=\"w-auto\"\n            >\n              <Button className=\"my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-blue-500 hover:bg-blue-600\">\n                Get your Wrapped\n              </Button>\n            </Link>\n          </div>\n        </div>\n        <div className=\"w-full lg:w-1/2 p-8\">\n          <div className=\"flex justify-center\">\n            <img src={wrapped} alt=\"preview\" />\n          </div>\n        </div>\n      </div>\n      <div className=\"bg-gray-200 text-gray-700 w-full flex flex-col justify-center items-center pt-16 py-4 px-4\">\n        <h1 className=\"text-4xl font-medium mb-4\">GitHub Trends</h1>\n        <h2 className=\"w-2/3 text-center text-lg\">\n          GitHub Trends dives deep into the GitHub API to bring you insightful\n          metrics and visualizations. We access individual commits to compute\n          accurate and granular statistics.\n        </h2>\n        <div className=\"w-4/5 mx-auto py-8 flex flex-wrap items-center justify-center\">\n          {[\n            {\n              header: 'Measures Contribs',\n              text: 'Calculates your stats on a per-contribution level, allowing for deeper insights',\n            },\n            {\n              header: 'LOC Insights',\n              text: 'See aggregate stats on lines of code (LOC) written across all contributions',\n            },\n            {\n              header: 'Language Breakdowns',\n              text: 'Showcase your favorite languages with LOC language breakdowns',\n            },\n            {\n              header: 'Private Mode',\n              text: 'Use a PAT to avoid rate limiting and include private contributions',\n            },\n            {\n              header: 'Exciting Visualizations',\n              text: 'Visualize your contributions with bar graphs, pie charts, and more',\n            },\n            {\n              header: 'Shareable Stats',\n              text: 'Easily add your cards to your GitHub and share them online',\n            },\n          ].map((item, index) => (\n            // eslint-disable-next-line react/no-array-index-key\n            <div className=\"flex w-full md:w-1/2 lg:w-1/3 p-4\" key={index}>\n              <div className=\"w-4 h-4 mt-1 mr-2\">\n                <CheckIcon className=\"w-full h-full text-green-600\" />\n              </div>\n              <div className=\"w-4/5 flex flex-col justify-top\">\n                <p className=\"text-xl mb-1 font-medium\">{item.header}</p>\n                <p>{item.text}</p>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n      {!isAuthenticated && (\n        <div className=\"text-gray-700 w-full flex flex-col justify-center items-center py-16 px-4\">\n          <p className=\"text-3xl font-medium\">Ready to get started?</p>\n          <p className=\"text-3xl font-medium\">Create an account today.</p>\n          <div className=\"mt-2\">\n            <Link to=\"/demo\" className=\"w-auto\">\n              <Button className=\"my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-gray-700 hover:bg-gray-800\">\n                Try Demo\n              </Button>\n            </Link>\n            <Link to=\"/signup\" className=\"w-auto\">\n              <Button className=\"my-4 mr-4 w-auto justify-center text-white text-xl 3xl:text-2xl bg-blue-500 hover:bg-blue-600\">\n                Sign Up\n              </Button>\n            </Link>\n          </div>\n        </div>\n      )}\n    </section>\n  );\n}\n\nexport default LandingScreen;\n"
  },
  {
    "path": "frontend/src/pages/Landing/index.js",
    "content": "import LandingScreen from './Landing';\n\nexport default LandingScreen;\n"
  },
  {
    "path": "frontend/src/pages/Misc/NoMatch.js",
    "content": "import React from 'react';\n\nimport { Link } from 'react-router-dom';\n\nimport { Button } from '../../components';\n\nconst NoMatchScreen = () => {\n  return (\n    <div className=\"h-full w-full flex flex-col justify-center items-center\">\n      <div className=\"flex\">\n        <div className=\"pr-8 text-5xl font-semibold text-blue-500\">404</div>\n        <div className=\"flex flex-col\">\n          <div className=\"pl-8 border-l-2 border-gray-200\">\n            <div className=\"text-5xl font-bold text-gray-900\">\n              Page not Found\n            </div>\n            <div className=\"text-lg text-gray-500 mt-2\">\n              Please check the URL in the address bar and try again.\n            </div>\n          </div>\n          <Link to=\"/\" className=\"ml-8 mt-8\">\n            <Button className=\"bg-blue-500 hover:bg-blue-600 text-white\">\n              Go to Home\n            </Button>\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default NoMatchScreen;\n"
  },
  {
    "path": "frontend/src/pages/Misc/Redirect.js",
    "content": "// eslint-disable-next-line no-unused-vars\nimport React, { useEffect } from 'react';\nimport { BACKEND_URL } from '../../constants';\n\nconst RedirectScreen = () => {\n  useEffect(() => {\n    async function redirectCode() {\n      // Take any query parameters and pass to redirect\n      const url = window.location.href;\n      const hasCode = url.includes('user/redirect');\n\n      // If Github API returns the code parameter\n      if (hasCode) {\n        const newUrl = url.split('user/redirect');\n        window.location.replace(`${BACKEND_URL}/auth/redirect${newUrl[1]}`);\n      }\n    }\n\n    redirectCode();\n  }, []);\n\n  return null;\n};\n\nexport default RedirectScreen;\n"
  },
  {
    "path": "frontend/src/pages/Misc/index.js",
    "content": "import NoMatchScreen from './NoMatch';\nimport RedirectScreen from './Redirect';\n\nexport { NoMatchScreen, RedirectScreen };\n"
  },
  {
    "path": "frontend/src/pages/Settings/Settings.js",
    "content": "/* eslint-disable jsx-a11y/click-events-have-key-events */\n/* eslint-disable jsx-a11y/no-static-element-interactions */\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport PropTypes from 'prop-types';\nimport { useSelector, useDispatch } from 'react-redux';\n\nimport { Button } from '../../components';\nimport { logout as _logout } from '../../redux/actions/userActions';\nimport { deleteAccount } from '../../api';\nimport { classnames } from '../../utils';\nimport { GITHUB_PRIVATE_AUTH_URL, CLIENT_ID } from '../../constants';\n\nconst SectionButton = ({ name, implemented, isSelected, setSelected }) => {\n  return (\n    <div\n      className={classnames(\n        'w-full py-3 px-3 hover:bg-gray-100 text-gray-700 text-lg cursor-pointer',\n        isSelected &&\n          'pl-2 bg-gray-100 cursor-default border-l-4 border-blue-500',\n        !implemented && 'opacity-50 cursor-not-allowed',\n      )}\n      onClick={() => setSelected('accountTier')}\n    >\n      {name}\n    </div>\n  );\n};\n\nSectionButton.propTypes = {\n  name: PropTypes.string.isRequired,\n  implemented: PropTypes.bool,\n  isSelected: PropTypes.bool.isRequired,\n  setSelected: PropTypes.func.isRequired,\n};\n\nSectionButton.defaultProps = {\n  implemented: true,\n};\n\nfunction useOutsideAlerter(ref, action) {\n  useEffect(() => {\n    /**\n     * Alert if clicked on outside of element\n     */\n    function handleClickOutside(event) {\n      if (ref.current && !ref.current.contains(event.target)) {\n        action();\n      }\n    }\n\n    // Bind the event listener\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => {\n      // Unbind the event listener on clean up\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [ref]);\n}\n\nconst SettingsScreen = () => {\n  const [selected, setSelected] = useState('accountTier');\n  const [deleteModal, setDeleteModal] = useState(false);\n\n  const openDeleteModal = () => {\n    setDeleteModal(true);\n  };\n\n  const closeDeleteModal = () => {\n    setDeleteModal(false);\n  };\n\n  const wrapperRef = useRef(null);\n  useOutsideAlerter(wrapperRef, closeDeleteModal);\n\n  const userId = useSelector((state) => state.user.userId);\n  const isAuthenticated = userId && userId.length > 0;\n  const userKey = useSelector((state) => state.user.userKey);\n  const privateAccess = useSelector((state) => state.user.privateAccess);\n  const accountTier = privateAccess ? 'Private Workflow' : 'Public Workflow';\n\n  const dispatch = useDispatch();\n  const logout = () => dispatch(_logout());\n\n  console.log(isAuthenticated, userId, userKey, privateAccess, accountTier);\n\n  if (!isAuthenticated) {\n    return (\n      <div className=\"h-full py-8 flex justify-center items-center\">\n        <div className=\"text-center\">\n          <h1 className=\"text-3xl font-bold\">\n            Please sign in to access this page\n          </h1>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"h-full w-full\">\n      <div className={classnames('h-full flex', deleteModal && 'opacity-25')}>\n        <div className=\"h-full w-1/3 md:w-80 p-4 flex flex-col border-r\">\n          <p className=\"p-2 text-gray-700 text-lg font-bold\">\n            Account Settings\n          </p>\n          <hr />\n          <SectionButton\n            name=\"Account Tier\"\n            isSelected={selected === 'accountTier'}\n            setSelected={() => setSelected('accountTier')}\n          />\n          <SectionButton\n            name=\"Personalization\"\n            implemented={false}\n            isSelected={selected === 'personalization'}\n            setSelected={() => setSelected('personalization')}\n          />\n          <SectionButton\n            name=\"Delete Account\"\n            isSelected={selected === 'deleteAccount'}\n            setSelected={() => setSelected('deleteAccount')}\n          />\n        </div>\n        <div className=\"h-full w-full\">\n          <div className=\"container mx-auto p-8\">\n            {selected === 'accountTier' && (\n              <div>\n                <p className=\"mb-1 text-2xl text-gray-700\">Account Tier</p>\n                <hr />\n                <br />\n                <p className=\"flex text-lg\">\n                  Current Tier:\n                  <strong className=\"ml-1\">{accountTier}</strong>\n                </p>\n                <br />\n                {privateAccess ? (\n                  <p>\n                    You have given GitHub Trends (read and write) access to all\n                    public and private code contributions. We use your GitHub\n                    API access token to make requests on your behalf. All of our\n                    code is open-source and visible on our\n                    <a\n                      className=\"ml-1 text-blue-500 underline\"\n                      href=\"https://github.com/avgupta456/github-trends\"\n                      rel=\"noopener noreferrer\"\n                      target=\"_blank\"\n                    >\n                      GitHub repository\n                    </a>\n                  </p>\n                ) : (\n                  <p>\n                    You have given GitHub Trends read access to your public\n                    repositories. Upgrading to the Private Workflow will allow\n                    us to better represent your code contributions. We use your\n                    GitHub API access token to make requests on your behalf. All\n                    of our code is open-source and visible on our\n                    <a\n                      className=\"ml-1 text-blue-500 underline\"\n                      href=\"https://github.com/avgupta456/github-trends\"\n                      rel=\"noopener noreferrer\"\n                      target=\"_blank\"\n                    >\n                      GitHub repository\n                    </a>\n                  </p>\n                )}\n                <br />\n                {privateAccess ? (\n                  <Button className=\"bg-gray-200 rounded-sm opacity-50 cursor-not-allowed\">\n                    Downgrade to Public Access\n                  </Button>\n                ) : (\n                  <a href={GITHUB_PRIVATE_AUTH_URL}>\n                    <Button className=\"bg-blue-500 text-white rounded-sm\">\n                      Upgrade to Private Access\n                    </Button>\n                  </a>\n                )}\n              </div>\n            )}\n            {selected === 'personalization' && (\n              <div>\n                <p className=\"mb-1 text-2xl text-gray-700\">Personalization</p>\n                <hr />\n                <br />\n                <p>Coming soon!</p>\n              </div>\n            )}\n            {selected === 'deleteAccount' && (\n              <div>\n                <p className=\"mb-1 text-2xl text-gray-700\">Delete Account</p>\n                <hr />\n                <br />\n                <p>\n                  Deleting your account is permanent and cannot be undone. If\n                  you are sure you want to delete your account, click the button\n                  below to remove your statistics from GitHub Trends. This will\n                  redirect you to a GitHub screen where you can revoke your\n                  access token.\n                </p>\n                <br />\n                <Button\n                  className=\"bg-gray-200 rounded-sm text-red-600 border-2\"\n                  onClick={openDeleteModal}\n                >\n                  Permenantly Delete Account\n                </Button>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n      {deleteModal && (\n        <div>\n          <div className=\"fixed left-0 top-0 w-full h-full\">\n            <div className=\"w-full h-full flex justify-center items-center\">\n              <div\n                className=\"w-96 p-4 bg-white rounded-sm border-2 border-gray-200\"\n                ref={wrapperRef}\n              >\n                <p className=\"mb-1 text-2xl text-gray-700\">Delete Account</p>\n                <hr />\n                <br />\n                <p>\n                  Are you sure you want to continue? This action cannot be\n                  undone.\n                </p>\n                <br />\n                <div className=\"flex flex-wrap\">\n                  <Button\n                    className=\"bg-blue-500 text-white rounded-sm\"\n                    onClick={() => setDeleteModal(false)}\n                  >\n                    Cancel\n                  </Button>\n                  <Button\n                    className=\"bg-gray-200 ml-auto rounded-sm text-red-600 border-2\"\n                    onClick={async () => {\n                      const success = await deleteAccount(userId, userKey);\n                      if (success) {\n                        logout();\n                        window.location = `https://github.com/settings/connections/applications/${CLIENT_ID}`;\n                      }\n                    }}\n                  >\n                    Delete Account\n                  </Button>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default SettingsScreen;\n"
  },
  {
    "path": "frontend/src/pages/Settings/index.js",
    "content": "import SettingsScreen from './Settings';\n\nexport default SettingsScreen;\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/SelectUser.js",
    "content": "/* eslint-disable no-alert */\n/* eslint-disable no-unused-vars */\n\nimport React, { useState, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport { useNavigate, Link } from 'react-router-dom';\nimport { ClipLoader } from 'react-spinners';\nimport { BsInfoCircle } from 'react-icons/bs';\nimport { FaGithub as GithubIcon, FaCheck as CheckIcon } from 'react-icons/fa';\n\nimport { getValidUser } from '../../api/wrapped';\nimport { Button, Preview } from '../../components';\nimport { classnames, sleep } from '../../utils';\nimport wrapped1 from '../../assets/wrapped1.png';\nimport wrapped2 from '../../assets/wrapped2.png';\nimport wrapped3 from '../../assets/wrapped3.png';\nimport { PROD } from '../../constants';\nimport { authenticate, setUserKey } from '../../api';\nimport { login as _login } from '../../redux/actions/userActions';\n\nconst SelectUserScreen = () => {\n  const userId = useSelector((state) => state.user.userId || '');\n\n  const [userName, setUserName] = useState(userId);\n\n  const navigate = useNavigate();\n  const dispatch = useDispatch();\n\n  let userNameInput;\n\n  useEffect(() => {\n    userNameInput.focus();\n  }, [userNameInput]);\n\n  const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey));\n\n  useEffect(() => {\n    async function redirectCode() {\n      // After requesting Github access, Github redirects back to your app with a code parameter\n      const url = window.location.href;\n\n      if (url.includes('error=')) {\n        navigate('/');\n      }\n\n      // If Github API returns the code parameter\n      if (url.includes('code=')) {\n        const tempPrivateAccess = url.includes('private');\n        const newUrl = url.split('?code=');\n        const subStr = PROD ? 'githubwrapped.io' : 'localhost:3001';\n        const redirect = `${url.split(subStr)[0]}${subStr}/`;\n        window.history.pushState({}, null, redirect);\n        const userKey = await setUserKey(newUrl[1]);\n        const newUserId = await authenticate(newUrl[1], tempPrivateAccess);\n        login(newUserId, userKey);\n      }\n    }\n\n    redirectCode();\n  }, []);\n\n  const [error, setError] = useState('');\n  const [isLoading, setIsLoading] = useState(false);\n\n  const handleSubmit = async () => {\n    setIsLoading(true);\n    const validUser = await getValidUser(userName);\n    if (validUser.includes('Valid user')) {\n      const newUserName = validUser.split(' ')[2];\n      await sleep(10);\n      navigate(`/${newUserName}`);\n    } else if (validUser === 'GitHub user not found') {\n      setError('GitHub user not found. Check your spelling and try again.');\n    } else if (validUser === 'Repo not starred') {\n      setError(\n        'This user has not starred the GitHub Trends repository. Please star the repo and try again.',\n      );\n    }\n    setIsLoading(false);\n  };\n\n  return (\n    <div className=\"w-full -mt-16 text-white\">\n      <div className=\"w-full h-full bg-blue-500 pt-24 pb-8\">\n        <div className=\"w-full text-center p-8 lg:pb-2\">\n          <h1 className=\"text-2xl md:text-3xl lg:text-4xl font-medium mb-2\">\n            Reflect on your year <br className=\"inline sm:hidden\" />\n            with <strong>GitHub Wrapped</strong>\n          </h1>\n          <p className=\"hidden sm:inline text-lg\">\n            Powered by{' '}\n            <strong>\n              <Link\n                to=\"https://www.githubtrends.io\"\n                className=\"underline\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                GitHub Trends\n              </Link>\n            </strong>{' '}\n            (not affiliated with GitHub)\n          </p>\n        </div>\n        <div className=\"w-full h-full flex flex-wrap items-center container mx-auto\">\n          <div className=\"w-full lg:w-1/2 xl:w-2/5 flex flex-col items-center\">\n            <div className=\"p-6 m-2 md:w-80 lg:w-96 rounded-lg bg-gray-200 shadow text-gray-800\">\n              <div className=\"text-sm lg:text-lg mb-4 flex items-center\">\n                <p>\n                  <strong>Step 1</strong>: Star the GitHub repository.{' '}\n                </p>\n                <div\n                  className=\"hidden md:inline md:tooltip\"\n                  data-tip=\"This helps prevent spam requests and protect user privacy. Feel free to unstar after.\"\n                >\n                  <BsInfoCircle className=\"h-4 w-4 ml-2 text-gray-500 hover:text-gray-800 cursor-pointer\" />\n                </div>\n              </div>\n              <div className=\"w-full flex flex-col items-center\">\n                <a\n                  href=\"https://www.github.com/avgupta456/github-trends\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  <Button className=\"bg-black text-white flex items-center\">\n                    Star on\n                    <GithubIcon className=\"ml-1.5 w-5 h-5\" />\n                  </Button>\n                </a>\n              </div>\n              <p className=\"text-sm lg:text-lg mt-8 mb-4\">\n                <strong>Step 2</strong>: Enter your GitHub username!\n              </p>\n              <div className=\"flex space-x-2 mt-2\">\n                <input\n                  type=\"text\"\n                  autoCapitalize=\"none\"\n                  ref={(input) => {\n                    userNameInput = input;\n                  }}\n                  placeholder=\"Enter Username\"\n                  className={classnames(\n                    'bg-white text-gray-700 w-full input input-bordered rounded-sm',\n                    error && 'input-error',\n                  )}\n                  defaultValue={userName}\n                  onChange={(e) => {\n                    setUserName(e.target.value);\n                    setError('');\n                  }}\n                  onKeyPress={async (e) => {\n                    if (e.key === 'Enter') {\n                      handleSubmit();\n                    }\n                  }}\n                />\n                <Button\n                  type=\"submit\"\n                  className=\"bg-blue-500 hover:bg-blue-700 text-white flex items-center\"\n                  onClick={handleSubmit}\n                >\n                  {isLoading ? (\n                    <ClipLoader size={22} color=\"#fff\" speedMultiplier={0.5} />\n                  ) : (\n                    'Go'\n                  )}\n                </Button>\n              </div>\n              {error ? (\n                <div className=\"text-red-500 text-sm mt-2\">\n                  <strong>Error:</strong> {error}\n                </div>\n              ) : (\n                <div className=\"text-sm mt-2 py-5\" />\n              )}\n            </div>\n          </div>\n          <div className=\"w-full lg:w-1/2 xl:w-3/5 lg:px-8 flex flex-col items-center\">\n            <div className=\"w-full xl:w-4/5 2xl:w-3/4 mx-auto\">\n              <Preview\n                pages={[wrapped1, wrapped2, wrapped3]}\n                details={[\n                  'Detailed metrics and insightful charts',\n                  'Lines of code metrics (by langs and repos)',\n                  'Over a dozen stats to reflect on your year',\n                ]}\n                showArrows={false}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"bg-white text-gray-700 w-full py-16\">\n        <div className=\"w-full container mx-auto flex flex-col items-center\">\n          <h1 className=\"text-4xl font-medium mb-4\">See some examples</h1>\n          <div className=\"w-full flex flex-wrap justify-center items-center px-4\">\n            {[\n              {\n                name: 'Linus Torvalds',\n                username: 'torvalds',\n                url: 'https://avatars.githubusercontent.com/u/1024025?v=4',\n                blurb: 'Creator of Linux',\n              },\n              {\n                name: 'Evan You',\n                username: 'yyx990803',\n                url: 'https://avatars.githubusercontent.com/u/499550?v=4',\n                blurb: 'Creator of Vue',\n              },\n              {\n                name: 'shadcn',\n                username: 'shadcn',\n                url: 'https://avatars.githubusercontent.com/u/124599?v=4',\n                blurb: 'Vercel, shadcn/ui',\n              },\n              {\n                name: 'Sindre Sorhus',\n                username: 'sindresorhus',\n                url: 'https://avatars.githubusercontent.com/u/170270?v=4',\n                blurb: 'Open-sourcer',\n              },\n            ].map((user) => (\n              <div className=\"w-full md:w-1/2 lg:w-1/4 p-4\" key={user.username}>\n                <Link to={`/${user.username}`}>\n                  <div className=\"w-full rounded bg-gray-50 hover:bg-gray-100 shadow p-4 flex\">\n                    <img\n                      src={user.url}\n                      alt={user.username}\n                      className=\"w-24 h-24 rounded-full mr-4 my-auto\"\n                    />\n                    <div className=\"w-full flex flex-col items-center\">\n                      <strong className=\"w-full text-center\">\n                        {user.name}\n                      </strong>\n                      <p className=\"w-full text-center\">{user.blurb}</p>\n                    </div>\n                  </div>\n                </Link>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n      <div className=\"bg-gray-200 text-gray-800 w-full pt-16\">\n        <div className=\"w-full container mx-auto flex flex-col items-center justify-center\">\n          <h1 className=\"text-4xl font-medium mb-4\">GitHub Trends</h1>\n          <h2 className=\"w-3/4 text-center text-sm lg:text-lg\">\n            GitHub Trends dives deep into the GitHub API to bring you insightful\n            metrics and visualizations. We access individual commits to compute\n            accurate and granular statistics.\n          </h2>\n          <div className=\"w-4/5 mx-auto py-8 flex flex-wrap items-center justify-center\">\n            {[\n              {\n                header: 'Measures Contribs',\n                text: 'Calculates your stats on a per-contribution level, allowing for deeper insights',\n              },\n              {\n                header: 'LOC Insights',\n                text: 'See aggregate stats on lines of code (LOC) written across all contributions',\n              },\n              {\n                header: 'Language Breakdowns',\n                text: 'Showcase your favorite languages with LOC language breakdowns',\n              },\n              {\n                header: 'Private Mode',\n                text: 'Use a PAT to avoid rate limiting and include private contributions',\n              },\n              {\n                header: 'Exciting Visualizations',\n                text: 'Visualize your contributions with bar graphs, pie charts, and more',\n              },\n              {\n                header: 'Shareable Stats',\n                text: 'Easily add your cards to your GitHub and share them online',\n              },\n            ].map((item, index) => (\n              // eslint-disable-next-line react/no-array-index-key\n              <div className=\"flex w-full md:w-1/2 lg:w-1/3 p-4\" key={index}>\n                <div className=\"w-4 h-4 mt-1 mr-2\">\n                  <CheckIcon className=\"w-full h-full text-green-600\" />\n                </div>\n                <div className=\"w-4/5 flex flex-col justify-top\">\n                  <p className=\"text-xl mb-1 font-medium\">{item.header}</p>\n                  <p>{item.text}</p>\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default SelectUserScreen;\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/Wrapped.js",
    "content": "/* eslint-disable react/jsx-one-expression-per-line */\n\nimport React, { useEffect, useState } from 'react';\nimport { useSelector } from 'react-redux';\n\nimport { useParams, Link } from 'react-router-dom';\nimport { toPng } from 'html-to-image';\nimport download from 'downloadjs';\nimport { FaArrowLeft as LeftArrowIcon } from 'react-icons/fa';\nimport { BsImage as ImageIcon, BsInfoCircle } from 'react-icons/bs';\nimport Select from 'react-select';\nimport { ClipLoader } from 'react-spinners';\n\nimport { getWrapped } from '../../api';\nimport {\n  WrappedSection,\n  Numeric,\n  NumericOutOf,\n  Calendar,\n  hoverScale,\n  singleHoverScale,\n  BarMonth,\n  BarDay,\n  PieLangs,\n  PieRepos,\n  SwarmDay,\n  NumericPlusLOC,\n  NumericMinusLOC,\n  NumericBothLOC,\n  NumericBestDay,\n} from '../../components';\nimport Radar from '../../components/Wrapped/Specifics/Radar';\nimport { LoadingScreen } from './sections';\nimport { classnames } from '../../utils';\nimport { CURR_YEAR } from '../../constants';\n\nconst WrappedScreen = () => {\n  // eslint-disable-next-line prefer-const\n  let { userId, year } = useParams();\n  year = year || `${CURR_YEAR}`;\n\n  const currUserId = useSelector((state) => state.user.userId);\n  const usePrivate = useSelector((state) => state.user.privateAccess);\n\n  const [data, setData] = useState({});\n  const [isLoading, setIsLoading] = useState(true);\n  const [highlightDays, setHighlightDays] = useState([]);\n  const [highlightColors, setHighlightColors] = useState(hoverScale);\n\n  const [downloadLoading, setDownloadLoading] = useState(false);\n\n  // eslint-disable-next-line no-unused-vars\n  const downloadImage = async () => {\n    const dataUrl = await toPng(document.getElementById('screenshot-div'));\n    download(dataUrl, 'github-wrapped.png');\n  };\n\n  useEffect(() => {\n    async function getData() {\n      if (userId?.length > 0 && year > 2010 && year <= CURR_YEAR) {\n        const output = await getWrapped(userId, year);\n        if (\n          output !== null &&\n          output !== undefined &&\n          Object.keys(output).length > 0\n        ) {\n          setData(output);\n          setIsLoading(false);\n        }\n      }\n    }\n    getData();\n  }, [userId, year]);\n\n  if (isLoading) {\n    return <LoadingScreen />;\n  }\n\n  const startStreak = data?.numeric_data?.misc?.longest_streak_days?.[0] || 0;\n  const endStreak = data?.numeric_data?.misc?.longest_streak_days?.[1] || 0;\n  const startGap = data?.numeric_data?.misc?.longest_gap_days?.[0] || 0;\n  const endGap = data?.numeric_data?.misc?.longest_gap_days?.[1] || 0;\n\n  const bestDayMonth =\n    data?.numeric_data?.misc?.best_day_date?.split('-')?.[1] || '-';\n  const bestDayDay =\n    data?.numeric_data?.misc?.best_day_date?.split('-')?.[2] || '-';\n  const bestDayYear =\n    data?.numeric_data?.misc?.best_day_date?.split('-')?.[0] || '-';\n\n  return (\n    <div className=\"containermx-auto\">\n      <div\n        className={classnames(\n          'h-full w-full bg-white flex flex-row flex-wrap justify-center items-center',\n          'px-2 lg:px-4 xl:px-16 py-4 lg:py-8',\n        )}\n        id=\"screenshot-div\"\n      >\n        <WrappedSection useTitle={false}>\n          <div className=\"w-full h-auto flex flex-row flex-wrap -mb-4\">\n            {!downloadLoading && (\n              <Link to=\"/\">\n                <LeftArrowIcon className=\"hidden md:block absolute ml-2 mt-2 h-8 w-8 text-gray-500 hover:text-gray-800\" />\n              </Link>\n            )}\n            <p className=\"text-xl font-semibold text-center w-full\">\n              {`${userId}'s`}\n            </p>\n            <div className=\"w-full flex justify-center items-center\">\n              <Select\n                options={Array.from(\n                  { length: 10 },\n                  (_, i) => CURR_YEAR - i,\n                ).map((x) => ({ value: x, label: x }))}\n                value={{ value: year, label: year }}\n                onChange={(e) => {\n                  window.location.href = `/${userId}/${e.value}`;\n                }}\n              />\n              <p className=\"text-2xl md:text-3xl ml-2\">GitHub Wrapped</p>\n            </div>\n            <div className=\"mt-2 text-md text-center w-full text-gray-600 flex justify-center items-center\">\n              Private Access:{' '}\n              {userId === currUserId && usePrivate ? 'True' : 'False'}\n              {!(userId === currUserId && usePrivate) && (\n                <div\n                  className=\"hidden md:inline md:tooltip\"\n                  data-tip=\"For private access, create an account with GitHub Trends and authenticate with GitHub.\"\n                >\n                  <BsInfoCircle className=\"h-4 w-4 ml-2 text-gray-500 hover:text-gray-800 cursor-pointer\" />\n                </div>\n              )}\n            </div>\n            {data?.incomplete && (\n              <p className=\"mt-2 text-md text-center w-full text-red-600\">\n                Incomplete Data. Please refresh later to finish loading.\n              </p>\n            )}\n          </div>\n        </WrappedSection>\n        <WrappedSection title=\"Contribution Calendar\">\n          <div className=\"w-full lg:w-4/5\">\n            <Calendar\n              data={data}\n              startDate={`${year}-01-02`}\n              endDate={`${year}-12-31`}\n              highlightDays={highlightDays}\n              highlightColors={highlightColors}\n              downloadLoading={downloadLoading}\n            />\n          </div>\n          <div className=\"w-1/2 md:w-1/4 lg:w-1/5\">\n            <NumericOutOf\n              num={data?.numeric_data?.misc?.total_days || 0}\n              outOf={365}\n              label=\"Active Days\"\n            />\n          </div>\n          <div className=\"w-1/2 md:w-1/4\">\n            <NumericOutOf\n              num={data?.numeric_data?.misc?.longest_streak || 0}\n              outOf={100}\n              label=\"Longest Streak\"\n              className=\"hover:bg-gray-200 cursor-pointer\"\n              onMouseOver={() => {\n                setHighlightDays(\n                  Array.from(\n                    { length: endStreak - startStreak + 1 },\n                    (_, i) => startStreak + i,\n                  ),\n                );\n              }}\n              onMouseOut={() => {\n                setHighlightDays([]);\n              }}\n            />\n          </div>\n          <div className=\"w-1/2 md:w-1/4\">\n            <NumericOutOf\n              num={data?.numeric_data?.misc?.longest_gap || 0}\n              outOf={100}\n              label=\"Longest Gap\"\n              color=\"#EF4444\"\n              className=\"hover:bg-gray-200 cursor-pointer\"\n              onMouseOver={() =>\n                setHighlightDays(\n                  Array.from(\n                    { length: endGap - startGap + 1 },\n                    (_, i) => startGap + i,\n                  ),\n                )\n              }\n              onMouseOut={() => setHighlightDays([])}\n            />\n          </div>\n          <div className=\"w-1/2 md:w-1/4\">\n            <NumericOutOf\n              num={data?.numeric_data?.misc?.weekend_percent}\n              outOf={100}\n              format={(x) => `${x}%`}\n              label=\"Weekend Activity\"\n              color=\"#468CBF\"\n              className=\"hover:bg-gray-200 cursor-pointer\"\n              onMouseOver={() => {\n                const Sunday = Array.from({ length: 55 }, (_, i) => i).map(\n                  (x) =>\n                    x * 7 -\n                    ((year % 7) + 6) +\n                    Math.max(0, Math.floor((2024 - year) / 4)),\n                );\n                const Saturday = Array.from({ length: 55 }, (_, i) => i).map(\n                  (x) =>\n                    x * 7 -\n                    (year % 7) +\n                    Math.max(0, Math.floor((2024 - year) / 4)),\n                );\n                setHighlightDays([...Sunday, ...Saturday]);\n              }}\n              onMouseOut={() => setHighlightDays([])}\n            />\n          </div>\n          <div className=\"hidden lg:block w-1/4\">\n            <NumericBestDay\n              num={data?.numeric_data?.misc?.best_day_count}\n              date={`${bestDayMonth}/${bestDayDay}/${bestDayYear}`}\n              label=\"Busiest Day\"\n              className=\"hover:bg-gray-200 cursor-pointer\"\n              onMouseOver={() => {\n                setHighlightColors(singleHoverScale);\n                setHighlightDays([data?.numeric_data?.misc?.best_day_index]);\n              }}\n              onMouseOut={() => {\n                setHighlightColors(hoverScale);\n                setHighlightDays([]);\n              }}\n            />\n          </div>\n        </WrappedSection>\n        <WrappedSection title=\"Lines of Code (LOC) Analysis\">\n          <div className=\"w-full md:w-1/2 xl:w-1/3\">\n            <PieLangs data={data} downloadLoading={downloadLoading} />\n          </div>\n          <div className=\"w-full md:w-1/2 xl:w-1/3\">\n            <PieRepos data={data} downloadLoading={downloadLoading} />\n          </div>\n          <div className=\"w-full xl:w-1/3 flex flex-wrap\">\n            <div className=\"w-full md:w-1/2 lg:w-1/4 xl:w-1/2\">\n              <NumericPlusLOC\n                num={data?.numeric_data?.loc?.loc_additions}\n                label=\"LOC Additions\"\n              />\n            </div>\n            <div className=\"w-full md:w-1/2 lg:w-1/4 xl:w-1/2\">\n              <NumericMinusLOC\n                num={data?.numeric_data?.loc?.loc_deletions}\n                label=\"LOC Deletions\"\n              />\n            </div>\n            <div className=\"w-full md:w-1/2 lg:w-1/4 xl:w-1/2\">\n              <NumericBothLOC\n                num1={data?.numeric_data?.loc?.loc_additions_per_commit}\n                num2={data?.numeric_data?.loc?.loc_deletions_per_commit}\n                label=\"Typical Commit\"\n              />\n            </div>\n            <div className=\"w-full md:w-1/2 lg:w-1/4 xl:w-1/2\">\n              <Numeric\n                num={data?.numeric_data?.loc?.loc_changed_per_day}\n                label=\"Lines Changed / Day\"\n              />\n            </div>\n          </div>\n        </WrappedSection>\n        <WrappedSection title=\"Contribution Breakdown\">\n          <div className=\"w-full lg:w-1/3\">\n            <Radar data={data} />\n          </div>\n          <div className=\"w-full lg:w-2/3\">\n            <BarMonth data={data} downloadLoading={downloadLoading} />\n          </div>\n          <div className=\"w-full lg:w-2/3\">\n            <BarDay data={data} downloadLoading={downloadLoading} />\n          </div>\n          <div className=\"w-full lg:w-1/3\">\n            <SwarmDay data={data} />\n          </div>\n        </WrappedSection>\n        {downloadLoading && (\n          <div className=\"text-center text-2xl md:text-3xl lg:text-4xl font-bold text-blue-500\">\n            Create your own at www.githubwrapped.io\n          </div>\n        )}\n      </div>\n      <div className=\"fixed bottom-2 right-2 md:bottom-4 md:right-4 lg:bottom-8 lg:right-8\">\n        <button\n          type=\"button\"\n          className=\"rounded-sm shadow bg-gray-700 hover:bg-gray-800 text-gray-50 px-3 py-2\"\n          onClick={() => {\n            setDownloadLoading(true);\n            setTimeout(() => {\n              downloadImage();\n              setDownloadLoading(false);\n            }, 10);\n          }}\n        >\n          {downloadLoading ? (\n            <div className=\"w-28 h-6 flex justify-center\">\n              <ClipLoader size={22} color=\"#fff\" speedMultiplier={0.5} />\n            </div>\n          ) : (\n            <div className=\"w-28 h-6 flex items-center\">\n              <p>Save Image</p>\n              <ImageIcon className=\"ml-1.5 w-5 h-5\" />\n            </div>\n          )}\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default WrappedScreen;\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/index.js",
    "content": "import SelectUserScreen from './SelectUser';\nimport WrappedScreen from './Wrapped';\n\nexport { SelectUserScreen, WrappedScreen };\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/sections/Loading.js",
    "content": "/* eslint-disable react/jsx-one-expression-per-line */\n\nimport React, { useState, useEffect } from 'react';\n\nimport { PulseLoader } from 'react-spinners';\nimport Typist from 'react-typist';\nimport TypistLoop from 'react-typist-loop';\n\nimport './loading.css';\n\nconst LoadingScreen = () => {\n  const [showLoadingMessage, setShowLoadingMessage] = useState(false);\n  const [showLoadingErrorMessage, setShowLoadingErrorMessage] = useState(false);\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setShowLoadingMessage(true);\n    }, 8000);\n    const timer2 = setTimeout(() => {\n      setShowLoadingErrorMessage(true);\n    }, 50000);\n    return () => {\n      clearTimeout(timer);\n      clearTimeout(timer2);\n    };\n  }, []);\n\n  return (\n    <div className=\"h-full py-8 flex flex-col justify-center items-center\">\n      {showLoadingErrorMessage ? (\n        <div className=\"w-96 bg-gray-50 shadow p-4 text-gray-700 text-center text-lg\">\n          Something went wrong. Please try again in a couple minutes or raise an\n          issue on{' '}\n          <a\n            href=\"https://github.com/avgupta456/github-trends/issues/new\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-blue-500 underline\"\n          >\n            GitHub\n          </a>\n          . Thank you!\n        </div>\n      ) : (\n        <>\n          <div className=\"mb-8\">\n            <PulseLoader color=\"#3B82F6\" speedMultiplier={0.5} />\n          </div>\n          {showLoadingMessage ? (\n            <TypistLoop interval={200}>\n              {[\n                'Loading your Data...',\n                'Crunching Numbers...',\n                'Analyzing Trends...',\n                'Drawing Figures...',\n                'Almost there!',\n              ].map((text, i) => (\n                <Typist\n                  key={text}\n                  cursor={{ blink: true }}\n                  className=\"font-typist text-center text-2xl\"\n                >\n                  <Typist.Delay ms={500 * i} />\n                  {text}\n                  <Typist.Delay ms={i === 4 ? 20000 : 3000} />\n                  <Typist.Backspace count={text.length} />\n                </Typist>\n              ))}\n            </TypistLoop>\n          ) : (\n            <div className=\"h-8\" />\n          )}\n        </>\n      )}\n    </div>\n  );\n};\n\nexport default LoadingScreen;\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/sections/LoadingV2.js",
    "content": "/* eslint-disable no-else-return */\nimport React, { useEffect, useState } from 'react';\n\nimport { SquareLoader } from 'react-spinners';\n\nconst LoadingScreen = () => {\n  const months = [\n    'Jan',\n    'Feb',\n    'Mar',\n    'Apr',\n    'May',\n    'June',\n    'July',\n    'Aug',\n    'Sep',\n    'Oct',\n    'Nov',\n    'Dec',\n  ];\n\n  // Should take max ~45 seconds, added extra 10 seconds to Dec wait time\n  const waitTime = [\n    2000, 2000, 2000, 2000, 3000, 3000, 3000, 4000, 4000, 4000, 6000, 20000,\n  ];\n\n  const [currMonth, setCurrMonth] = useState(0);\n\n  // increment currMonth every 5 seconds\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCurrMonth(currMonth + 1);\n    }, waitTime[currMonth]);\n    return () => clearInterval(interval);\n  }, [currMonth]);\n\n  const getTile = (i) => {\n    if (i < currMonth) {\n      return (\n        <div\n          key={months[i]}\n          className=\"w-16 h-16 rounded bg-blue-500 m-1.5 text-center flex flex-col justify-center\"\n        >\n          <div className=\"text-white\">{months[i]}</div>\n        </div>\n      );\n    } else if (i === currMonth) {\n      return (\n        <SquareLoader\n          key={months[i]}\n          color=\"#3A82F6\"\n          speedMultiplier={0.75}\n          size={64}\n          className=\"rounded bg-blue-500 m-1.5\"\n        />\n      );\n    } else {\n      return (\n        <div\n          key={months[i]}\n          className=\"w-16 h-16 rounded bg-gray-500 m-1.5 text-center flex flex-col justify-center\"\n        >\n          <div className=\"text-white\">{months[i]}</div>\n        </div>\n      );\n    }\n  };\n\n  return (\n    <div className=\"h-full py-8 flex flex-col justify-center items-center\">\n      {currMonth < 12 ? (\n        <>\n          <div>Querying the GitHub API by Months</div>\n          <div className=\"flex flex-wrap m-4 justify-center\">\n            <div className=\"flex flex-wrap justify-center\">\n              <div className=\"flex\">\n                {Array.from({ length: 3 }).map((_, i) => getTile(i))}\n              </div>\n              <div className=\"flex\">\n                {Array.from({ length: 3 }).map((_, i) => getTile(i + 3))}\n              </div>\n            </div>\n            <div className=\"flex flex-wrap justify-center\">\n              <div className=\"flex\">\n                {Array.from({ length: 3 }).map((_, i) => getTile(i + 6))}\n              </div>\n              <div className=\"flex\">\n                {Array.from({ length: 3 }).map((_, i) => getTile(i + 9))}\n              </div>\n            </div>\n          </div>\n        </>\n      ) : (\n        <div className=\"w-4/5 lg:w-1/3 bg-gray-50 shadow p-4 text-gray-700 text-center text-lg\">\n          Loading your data is taking longer than expected. Try refreshing the\n          page, and if that fails, raise an issue on{' '}\n          <a\n            href=\"https://github.com/avgupta456/github-trends/issues/new\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-blue-500 underline\"\n          >\n            GitHub\n          </a>\n          . Thank you for your patience!\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default LoadingScreen;\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/sections/index.js",
    "content": "import LoadingScreen from './Loading';\n\nexport { LoadingScreen };\n"
  },
  {
    "path": "frontend/src/pages/Wrapped/sections/loading.css",
    "content": ".Typist .Cursor {\n  display: inline-block;\n}\n.Typist .Cursor--blinking {\n  opacity: 1;\n  animation: blink 1s linear infinite;\n}\n\n@keyframes blink {\n  0% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "frontend/src/redux/actions/userActions.js",
    "content": "export const LOGIN = 'LOGIN';\nexport const LOGOUT = 'LOGOUT';\nexport const SET_PRIVATE_ACCESS = 'SET_PRIVATE_ACCESS';\n\nexport function login(userId, userKey) {\n  return { type: LOGIN, payload: { userId, userKey } };\n}\n\nexport function logout() {\n  return { type: LOGOUT, payload: {} };\n}\n\nexport function setPrivateAccess(privateAccess) {\n  return { type: SET_PRIVATE_ACCESS, payload: { privateAccess } };\n}\n"
  },
  {
    "path": "frontend/src/redux/logger.js",
    "content": "// eslint-disable-next-line no-unused-vars\nconst logger = (store) => (next) => (action) => {\n  // console.group(action.type);\n  // console.info('dispatching', action);\n  const result = next(action);\n  // console.log('next state', store.getState());\n  console.groupEnd();\n  return result;\n};\n\nexport default logger;\n"
  },
  {
    "path": "frontend/src/redux/reducers/index.js",
    "content": "import { combineReducers } from 'redux';\n\nimport user from './user';\n\nexport default combineReducers({ user });\n"
  },
  {
    "path": "frontend/src/redux/reducers/user.js",
    "content": "import * as types from '../actions/userActions';\n\nconst initialState = {\n  userId: JSON.parse(localStorage.getItem('userId')) || null,\n  userKey: JSON.parse(localStorage.getItem('userKey')) || null,\n  privateAccess: null,\n};\n\n// eslint-disable-next-line default-param-last\nexport default (state = initialState, action) => {\n  switch (action.type) {\n    case types.LOGIN:\n      localStorage.setItem('userId', JSON.stringify(action.payload.userId));\n      localStorage.setItem('userKey', JSON.stringify(action.payload.userKey));\n      return {\n        ...state,\n        userId: action.payload.userId,\n        userKey: action.payload.userKey,\n      };\n    case types.LOGOUT:\n      localStorage.clear();\n      return {\n        userId: null,\n        userKey: null,\n        privateAccess: null,\n      };\n    case types.SET_PRIVATE_ACCESS:\n      return {\n        ...state,\n        privateAccess: action.payload.privateAccess,\n      };\n    default:\n      return state;\n  }\n};\n"
  },
  {
    "path": "frontend/src/redux/store.js",
    "content": "import { applyMiddleware, createStore, compose } from 'redux';\n\nimport loggerMiddleware from './logger';\nimport rootReducer from './reducers';\nimport { USE_LOGGER } from '../constants';\n\nexport default function configureStore(intialState) {\n  let middlewares = [];\n  if (USE_LOGGER) {\n    middlewares = [loggerMiddleware];\n  }\n  const middlewareEnhancer = applyMiddleware(...middlewares);\n\n  const enhancers = [middlewareEnhancer];\n  const composedEnhancers = compose(...enhancers);\n\n  const store = createStore(rootReducer, intialState, composedEnhancers);\n\n  return store;\n}\n"
  },
  {
    "path": "frontend/src/utils.js",
    "content": "export function sleep(ms) {\n  // eslint-disable-next-line no-promise-executor-return\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport function classnames(...args) {\n  return args.join(' ');\n}\n"
  },
  {
    "path": "frontend/tailwind.config.js",
    "content": "/* eslint-disable global-require */\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: ['./src/**/*.{js,jsx,ts,tsx}'],\n  theme: {\n    screens: {\n      sm: '640px',\n      md: '768px',\n      lg: '1024px',\n      xl: '1280px',\n      '2xl': '1536px',\n      '3xl': '1792px',\n    },\n    extend: {\n      fontFamily: {\n        typist: ['Playfair Display SC', 'Courier New'],\n      },\n    },\n  },\n  plugins: [require('daisyui')],\n};\n"
  }
]