Repository: avgupta456/github-trends Branch: main Commit: 7d1ec357a103 Files: 238 Total size: 399.1 KB Directory structure: gitextract_vuc7bnqv/ ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── backend.yaml │ └── frontend.yaml ├── .gitignore ├── LICENSE ├── README.md ├── backend/ │ ├── .coveragerc │ ├── .env-template │ ├── .flake8 │ ├── .gcloudignore │ ├── .pre-commit-config.yaml │ ├── README.md │ ├── deploy/ │ │ ├── app.yaml │ │ ├── cloudbuild.yaml │ │ └── dispatch.yaml │ ├── package.json │ ├── pyproject.toml │ ├── requirements.txt │ ├── scripts/ │ │ ├── __init__.py │ │ ├── delete_old_data.py │ │ └── local.py │ ├── src/ │ │ ├── __init__.py │ │ ├── aggregation/ │ │ │ ├── layer0/ │ │ │ │ ├── __init__.py │ │ │ │ ├── contributions.py │ │ │ │ ├── follows.py │ │ │ │ ├── languages.py │ │ │ │ └── package.py │ │ │ ├── layer1/ │ │ │ │ ├── __init__.py │ │ │ │ ├── auth.py │ │ │ │ └── user.py │ │ │ └── layer2/ │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ └── user.py │ │ ├── constants.py │ │ ├── data/ │ │ │ ├── __init__.py │ │ │ ├── github/ │ │ │ │ ├── __init__.py │ │ │ │ ├── auth/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── main.py │ │ │ │ ├── extensions.json │ │ │ │ ├── graphql/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── commit.py │ │ │ │ │ ├── models.py │ │ │ │ │ ├── repo.py │ │ │ │ │ ├── template.py │ │ │ │ │ └── user/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── contribs/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── contribs.py │ │ │ │ │ │ └── models.py │ │ │ │ │ └── follows/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── follows.py │ │ │ │ │ └── models.py │ │ │ │ ├── language_map.py │ │ │ │ ├── rest/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── commit.py │ │ │ │ │ ├── models.py │ │ │ │ │ ├── repo.py │ │ │ │ │ ├── template.py │ │ │ │ │ └── user.py │ │ │ │ └── utils.py │ │ │ └── mongo/ │ │ │ ├── __init__.py │ │ │ ├── main.py │ │ │ ├── secret/ │ │ │ │ ├── __init__.py │ │ │ │ ├── functions.py │ │ │ │ └── models.py │ │ │ ├── user/ │ │ │ │ ├── __init__.py │ │ │ │ ├── functions.py │ │ │ │ ├── get.py │ │ │ │ └── models.py │ │ │ └── user_months/ │ │ │ ├── __init__.py │ │ │ ├── functions.py │ │ │ ├── get.py │ │ │ └── models.py │ │ ├── main.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ ├── background.py │ │ │ ├── svg.py │ │ │ ├── user/ │ │ │ │ ├── __init__.py │ │ │ │ ├── contribs.py │ │ │ │ ├── follows.py │ │ │ │ └── main.py │ │ │ └── wrapped/ │ │ │ ├── __init__.py │ │ │ ├── calendar.py │ │ │ ├── langs.py │ │ │ ├── main.py │ │ │ ├── numeric.py │ │ │ ├── repos.py │ │ │ ├── time.py │ │ │ └── timestamps.py │ │ ├── processing/ │ │ │ ├── auth.py │ │ │ ├── user/ │ │ │ │ ├── __init__.py │ │ │ │ ├── commits.py │ │ │ │ └── svg.py │ │ │ └── wrapped/ │ │ │ ├── __init__.py │ │ │ ├── calendar.py │ │ │ ├── langs.py │ │ │ ├── main.py │ │ │ ├── numeric.py │ │ │ ├── package.py │ │ │ ├── repos.py │ │ │ ├── time.py │ │ │ └── timestamps.py │ │ ├── render/ │ │ │ ├── __init__.py │ │ │ ├── error.py │ │ │ ├── style.py │ │ │ ├── template.py │ │ │ ├── top_langs.py │ │ │ └── top_repos.py │ │ ├── routers/ │ │ │ ├── __init__.py │ │ │ ├── assets/ │ │ │ │ ├── __init__.py │ │ │ │ └── assets.py │ │ │ ├── auth/ │ │ │ │ ├── __init__.py │ │ │ │ ├── main.py │ │ │ │ ├── standalone.py │ │ │ │ └── website.py │ │ │ ├── background.py │ │ │ ├── decorators.py │ │ │ ├── dev.py │ │ │ ├── users/ │ │ │ │ ├── __init__.py │ │ │ │ ├── db.py │ │ │ │ ├── main.py │ │ │ │ └── svg.py │ │ │ └── wrapped.py │ │ └── utils/ │ │ ├── __init__.py │ │ ├── alru_cache.py │ │ ├── decorators.py │ │ ├── gather.py │ │ └── utils.py │ ├── tests/ │ │ ├── __init__.py │ │ ├── aggregation/ │ │ │ ├── __init__.py │ │ │ └── layer0/ │ │ │ ├── __init__.py │ │ │ ├── test_contributions.py │ │ │ └── test_follows.py │ │ ├── data/ │ │ │ ├── __init__.py │ │ │ └── github/ │ │ │ ├── __init__.py │ │ │ ├── auth/ │ │ │ │ ├── __init__.py │ │ │ │ └── test_main.py │ │ │ ├── graphql/ │ │ │ │ ├── __init__.py │ │ │ │ ├── test_commits.py │ │ │ │ ├── test_repo.py │ │ │ │ ├── test_user_contribs.py │ │ │ │ └── test_user_follows.py │ │ │ └── rest/ │ │ │ ├── __init__.py │ │ │ ├── test_commit.py │ │ │ └── test_repo.py │ │ └── utils/ │ │ ├── __init__.py │ │ └── test_alru_cache.py │ └── transfer_mongodb.bash ├── docs/ │ ├── API.md │ ├── CONTRIBUTING.md │ ├── FAQ.md │ └── THEME.md └── frontend/ ├── .env-template ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .yarnrc ├── README.md ├── deploy/ │ └── Dockerfile ├── package.json ├── public/ │ ├── _redirects │ ├── manifest.json │ ├── robots.txt │ ├── trends.html │ └── wrapped.html ├── src/ │ ├── api/ │ │ ├── index.js │ │ ├── user.js │ │ └── wrapped.js │ ├── assets/ │ │ └── notes.txt │ ├── components/ │ │ ├── Card/ │ │ │ ├── Card.js │ │ │ ├── SVG.js │ │ │ └── index.js │ │ ├── Generic/ │ │ │ ├── Button.js │ │ │ ├── Checkbox.js │ │ │ ├── Input.js │ │ │ └── index.js │ │ ├── Home/ │ │ │ ├── CheckboxSection.js │ │ │ ├── DateRangeSection.js │ │ │ ├── Progress.js │ │ │ ├── Section.js │ │ │ └── index.js │ │ ├── Preview/ │ │ │ ├── Preview.js │ │ │ └── index.js │ │ ├── Wrapped/ │ │ │ ├── Organization.js │ │ │ ├── Specifics/ │ │ │ │ ├── Bar.js │ │ │ │ ├── Calendar.js │ │ │ │ ├── Numeric.js │ │ │ │ ├── Pie.js │ │ │ │ ├── Radar.js │ │ │ │ ├── Swarm.js │ │ │ │ └── index.js │ │ │ ├── Templates/ │ │ │ │ ├── Bar.js │ │ │ │ ├── Numeric.js │ │ │ │ ├── Pie.js │ │ │ │ ├── Swarm.js │ │ │ │ ├── index.js │ │ │ │ └── theme.js │ │ │ └── index.js │ │ └── index.js │ ├── constants.js │ ├── index.css │ ├── index.js │ ├── pages/ │ │ ├── App/ │ │ │ ├── AppTrends.js │ │ │ ├── AppWrapped.js │ │ │ ├── Footer.js │ │ │ ├── Header.js │ │ │ └── index.js │ │ ├── Auth/ │ │ │ ├── SignUp.js │ │ │ └── index.js │ │ ├── Demo/ │ │ │ ├── Demo.js │ │ │ └── index.js │ │ ├── Home/ │ │ │ ├── Home.js │ │ │ ├── index.js │ │ │ └── stages/ │ │ │ ├── Customize.js │ │ │ ├── Display.js │ │ │ ├── SelectCard.js │ │ │ ├── Theme.js │ │ │ └── index.js │ │ ├── Landing/ │ │ │ ├── Landing.js │ │ │ └── index.js │ │ ├── Misc/ │ │ │ ├── NoMatch.js │ │ │ ├── Redirect.js │ │ │ └── index.js │ │ ├── Settings/ │ │ │ ├── Settings.js │ │ │ └── index.js │ │ └── Wrapped/ │ │ ├── SelectUser.js │ │ ├── Wrapped.js │ │ ├── index.js │ │ └── sections/ │ │ ├── Loading.js │ │ ├── LoadingV2.js │ │ ├── index.js │ │ └── loading.css │ ├── redux/ │ │ ├── actions/ │ │ │ └── userActions.js │ │ ├── logger.js │ │ ├── reducers/ │ │ │ ├── index.js │ │ │ └── user.js │ │ └── store.js │ └── utils.js └── tailwind.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/workflows/backend.yaml ================================================ name: CI-Backend on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.11 uses: actions/setup-python@v2 with: python-version: 3.11 - name: Install dependencies run: | cd backend python -m pip install --upgrade pip pip install -r requirements.txt - name: Test with unittest run: | cd backend python -m unittest env: AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }} - name: Upload coverage to Coveralls run: | cd backend coverage run --source=src -m unittest coveralls env: AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} MONGODB_PASSWORD: ${{ secrets.MONGODB_PASSWORD }} ================================================ FILE: .github/workflows/frontend.yaml ================================================ name: CI-Frontend on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install modules run: | cd frontend yarn - name: Run ESLint run: | cd frontend yarn eslint . ================================================ FILE: .gitignore ================================================ *.pyc __pycache__ .vscode backend/.env backend/.venv backend/.coverage backend/gcloud_key.json frontend/.env .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Abhijit Gupta Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # GitHub Trends ## SPECIAL: GitHub Wrapped Check out your GitHub Wrapped at `githubwrapped.io`! ![github-wrapped](https://github.com/avgupta456/github-trends/assets/16708871/bf9406a4-6a49-4dbf-8f60-af221bb84bd6) --- ## What is GitHub Trends GitHub 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: ## Quickstart First, 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. ```md [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs)](https://githubtrends.io) ``` And voila, you get a card like above. Keep reading to learn more! ## Why GitHub Trends? Unlike 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. # Usage ## Website Workflow (Alpha) Visit [githubtrends.io](https://www.githubtrends.io) to create an account and get started! Have questions? Check out [the demo](https://www.githubtrends.io/demo)! ![image](https://user-images.githubusercontent.com/16708871/138611082-105e4dbc-8a27-4f68-8045-f9d86c912429.png) --- ## API Workflow (Alpha) Alternatively, 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. ## FAQ See [docs/FAQ.md](https://github.com/avgupta456/github-trends/blob/main/docs/FAQ.md). ## Contributing See [docs/CONTRIBUTING.md](https://github.com/avgupta456/github-trends/blob/main/docs/CONTRIBUTING.md). ## Acknowledgements Much 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! ================================================ FILE: backend/.coveragerc ================================================ [run] source = src omit = ./.venv/* ./tests/* ./models/* */__init__.py [report] omit = ./.venv/* ./tests/* ./models/* */__init__.py ================================================ FILE: backend/.env-template ================================================ AUTH_TOKEN=abc123 COVERALLS_REPO_TOKEN=abc123 PROD_OAUTH_CLIENT_ID=abc123 PROD_OAUTH_CLIENT_SECRET=abc123 PROD_OAUTH_REDIRECT_URI=abc123 DEV_OAUTH_CLIENT_ID=abc123 DEV_OAUTH_CLIENT_SECRET=abc123 DEV_OAUTH_REDIRECT_URI=abc123 GOOGLE_APPLICATION_CREDENTIALS=abc123 MONGODB_PASSWORD=abc123 ================================================ FILE: backend/.flake8 ================================================ [flake8] max-line-length = 88 max-complexity = 100 select = B,C,E,F,W,T ignore = E203, W503, E501 ================================================ FILE: backend/.gcloudignore ================================================ # This file specifies files that are *not* uploaded to Google Cloud Platform # using gcloud. It follows the same syntax as .gitignore, with the addition of # "#!include" directives (which insert the entries of the given .gitignore-style # file at that point). # # For more information, run: # $ gcloud topic gcloudignore # .gcloudignore # If you would like to upload your .git directory, .gitignore file or files # from your .gitignore file, remove the corresponding line # below: .git .gitignore # Python pycache: __pycache__/ .venv .coverage .coveragerc .flake8 poetry.lock pyproject.toml README.md # Ignored by the build system /setup.cfg # remove irrelevant files/folders deploy tests .env-template .pre-commit-config.yaml gcloud_key.json ================================================ FILE: backend/.pre-commit-config.yaml ================================================ repos: - repo: https://github.com/ambv/black rev: 21.10b0 hooks: - id: black language_version: python3.11 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - id: flake8 args: [--config=./backend/.flake8] ================================================ FILE: backend/README.md ================================================ # Backend ## Installation ``` poetry install poetry run pre-commit install ``` ## Run Locally Navigate to localhost:8000 ``` yarn start ``` ## Test with Code Coverage ``` yarn test ``` View coverage with GitHub badge or on coveralls.io ## Build If a new requirement has been added, make sure to update the requirements.txt ``` yarn set-reqs ``` Then, just commit on the main branch (Google Cloud Run takes care of the rest) ## Adding a Secret Update cloudbuild.yaml, .env, .env-template, and GCP Cloud Run Trigger Substitution Variables. ================================================ FILE: backend/deploy/app.yaml ================================================ service: default runtime: python311 entrypoint: gunicorn -w 2 -k uvicorn.workers.UvicornWorker src.main:app #smallest instance class instance_class: F1 #prevents creating additional instances automatic_scaling: min_instances: 0 max_instances: 1 env_variables: PROD: true ================================================ FILE: backend/deploy/cloudbuild.yaml ================================================ steps: - name: node:10.15.1 entrypoint: npm args: ["install"] dir: "backend" - name: node:10.15.1 entrypoint: npm args: ["run", "create-env"] dir: "backend" env: - "DEV_OAUTH_CLIENT_ID=${_DEV_OAUTH_CLIENT_ID}" - "DEV_OAUTH_CLIENT_SECRET=${_DEV_OAUTH_CLIENT_SECRET}" - "DEV_OAUTH_REDIRECT_URI=${_DEV_OAUTH_REDIRECT_URI}" - "PROD_OAUTH_CLIENT_ID=${_PROD_OAUTH_CLIENT_ID}" - "PROD_OAUTH_CLIENT_SECRET=${_PROD_OAUTH_CLIENT_SECRET}" - "PROD_OAUTH_REDIRECT_URI=${_PROD_OAUTH_REDIRECT_URI}" - "MONGODB_PASSWORD=${_MONGODB_PASSWORD}" - "SENTRY_DSN=${_SENTRY_DSN}" - name: "gcr.io/cloud-builders/gcloud" args: ["app", "deploy", "--appyaml", "./deploy/app.yaml"] dir: "backend" ================================================ FILE: backend/deploy/dispatch.yaml ================================================ dispatch: - url: "*/.*" service: default ================================================ FILE: backend/package.json ================================================ { "name": "github-trends", "version": "0.0.1", "private": true, "scripts": { "gen-lang-map": "poetry run python src/data/github/language_map.py", "start": "poetry run uvicorn src.main:app --reload --port=8000", "set-reqs": "poetry lock && poetry export -f requirements.txt --output requirements.txt --without-hashes", "create-env": "printenv > .env", "test": "poetry run coverage run --source=src -m unittest -v && poetry run coverage report", "isort": "poetry run isort . --src-path=./src --multi-line=3 --trailing-comma --line-length=88 --combine-as --ensure-newline-before-comments" } } ================================================ FILE: backend/pyproject.toml ================================================ [tool.poetry] name = "github-trends" version = "0.1.0" description = "" authors = ["Abhijit Gupta "] license = "MIT" [tool.poetry.dependencies] python = "^3.11" fastapi = "^0.104.1" uvicorn = {extras = ["standard"], version = "^0.24.0.post1"} requests = "^2.31.0" python-dotenv = "^1.0.0" motor = "^3.3.1" aiofiles = "^23.2.1" aiounittest = "^1.4.2" coveralls = "^3.3.1" grpcio = "^1.59.2" gunicorn = "^21.2.0" pymongo = {extras = ["srv"], version = "^4.6.0"} pytz = "^2023.3.post1" sentry-sdk = "^1.34.0" svgwrite = "^1.4.3" [tool.poetry.dev-dependencies] [tool.poetry.group.dev.dependencies] black = "^23.11.0" flake8 = "^6.1.0" isort = "^5.12.0" pre-commit = "^3.5.0" pyinstrument = "^4.6.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: backend/requirements.txt ================================================ aiofiles==23.2.1 aiounittest==1.4.2 annotated-types==0.6.0 anyio==3.7.1 certifi==2023.11.17 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 coverage==6.5.0 coveralls==3.3.1 dnspython==2.4.2 docopt==0.6.2 fastapi==0.104.1 grpcio==1.59.3 gunicorn==21.2.0 h11==0.14.0 httptools==0.6.1 idna==3.4 motor==3.3.2 packaging==23.2 pydantic-core==2.14.5 pydantic==2.5.2 pymongo==4.6.0 pymongo[srv]==4.6.0 python-dotenv==1.0.0 pytz==2023.3.post1 pyyaml==6.0.1 requests==2.31.0 sentry-sdk==1.36.0 sniffio==1.3.0 starlette==0.27.0 svgwrite==1.4.3 typing-extensions==4.8.0 urllib3==2.1.0 uvicorn[standard]==0.24.0.post1 uvloop==0.19.0 watchfiles==0.21.0 websockets==12.0 wrapt==1.16.0 ================================================ FILE: backend/scripts/__init__.py ================================================ ================================================ FILE: backend/scripts/delete_old_data.py ================================================ import asyncio import os import sys from datetime import datetime from typing import Any from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv()) # Add the parent directory to the Python path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) # flake8: noqa E402 from src.constants import API_VERSION from src.data.mongo.main import USER_MONTHS def get_filters(cutoff_date: datetime) -> Any: return { "$or": [ {"month": {"$lte": cutoff_date}}, {"version": {"$ne": API_VERSION}}, ], } async def count_old_rows(cutoff_date: datetime) -> int: filters = get_filters(cutoff_date) num_rows = len(await USER_MONTHS.find(filters).to_list(length=None)) # type: ignore return num_rows async def delete_old_rows(cutoff_date: datetime): filters = get_filters(cutoff_date) result = await USER_MONTHS.delete_many(filters) print(f"Deleted {result.deleted_count} rows") async def main(): # Replace 'your_date_field' with the actual name of your date field cutoff_date = datetime(2024, 12, 31) count = await count_old_rows(cutoff_date) if count == 0: print("No rows to delete.") return print(f"Found {count} rows to delete.") print() confirmation = input("Are you sure you want to delete these rows? (yes/no): ") if confirmation.lower() != "yes": print("Operation canceled.") return print() await delete_old_rows(cutoff_date) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) ================================================ FILE: backend/scripts/local.py ================================================ import argparse import asyncio import json import os import sys from datetime import datetime # Add the parent directory to the Python path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) os.environ["LOCAL"] = "True" # flake8: noqa E402 from src.aggregation.layer0 import get_user_data from src.processing.user import get_top_languages, get_top_repos from src.processing.wrapped.package import get_wrapped_data def parse_args(): parser = argparse.ArgumentParser(description="GitHub Trends Script") parser.add_argument("--user_id", required=True, help="GitHub user ID", type=str) parser.add_argument( "--access_token", required=True, help="GitHub access token", type=str ) parser.add_argument( "--start_date", default="2023-01-01", help="Start date in YYYY-MM-DD format", type=str, ) parser.add_argument( "--end_date", default="2023-01-31", help="End date in YYYY-MM-DD format", type=str, ) parser.add_argument( "--timezone", default="America/New_York", help="Timezone", type=str ) parser.add_argument( "--output_dir", default="./", help="Output directory path", type=str ) return parser.parse_args() async def main(): args = parse_args() start_date = datetime.strptime(args.start_date, "%Y-%m-%d") end_date = datetime.strptime(args.end_date, "%Y-%m-%d") print("Local script running...") print("User ID:", args.user_id) print("Access token:", args.access_token) print("Start date:", start_date) print("End date:", end_date) print("Timezone:", args.timezone) print("Output directory:", args.output_dir) print() raw_output = await get_user_data( args.user_id, start_date, end_date, args.timezone, args.access_token ) with open(os.path.join(args.output_dir, "raw.json"), "w") as f: f.write(raw_output.model_dump_json(indent=2)) langs_output = get_top_languages( raw_output, loc_metric="changed", include_private=True ) langs_output = ( [json.loads(x.model_dump_json()) for x in langs_output[0]], langs_output[1], ) repos_output = get_top_repos( raw_output, loc_metric="changed", include_private=True, group="none" ) repos_output = ( [json.loads(x.model_dump_json()) for x in repos_output[0]], repos_output[1], ) with open(os.path.join(args.output_dir, "langs.json"), "w") as f: f.write(json.dumps(langs_output, indent=2)) with open(os.path.join(args.output_dir, "repos.json"), "w") as f: f.write(json.dumps(repos_output, indent=2)) wrapped_user = get_wrapped_data(raw_output, 2023) with open(os.path.join(args.output_dir, "wrapped.json"), "w") as f: f.write(wrapped_user.model_dump_json(indent=2)) print("Wrote output to", args.output_dir) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) ================================================ FILE: backend/src/__init__.py ================================================ ================================================ FILE: backend/src/aggregation/layer0/__init__.py ================================================ from src.aggregation.layer0.package import get_user_data __all__ = ["get_user_data"] ================================================ FILE: backend/src/aggregation/layer0/contributions.py ================================================ from collections import defaultdict from datetime import date, datetime from typing import Any, Dict, List, Optional, Tuple, Union import pytz from src.aggregation.layer0.languages import CommitLanguages, get_commit_languages from src.constants import ( GRAPHQL_NODE_CHUNK_SIZE, GRAPHQL_NODE_THREADS, NODE_QUERIES, PR_FILES, REST_NODE_THREADS, ) from src.data.github.graphql import ( RawCalendar, RawCommit as GraphQLRawCommit, RawEventsCommit, RawEventsEvent, RawRepo, get_commits, get_repo, get_user_contribution_calendar, get_user_contribution_events, ) from src.data.github.rest import ( RawCommit as RESTRawCommit, RawCommitFile, get_commit_files, get_repo_commits, ) from src.models import UserContributions from src.utils import date_to_datetime, gather class ContribsList: def __init__(self): self.commits: List[RawEventsCommit] = [] self.issues: List[RawEventsEvent] = [] self.prs: List[RawEventsEvent] = [] self.reviews: List[RawEventsEvent] = [] self.repos: List[RawEventsEvent] = [] def add(self, label: str, event: Union[RawEventsCommit, RawEventsEvent]): if label == "commit" and isinstance(event, RawEventsCommit): self.commits.append(event) elif label == "issue" and isinstance(event, RawEventsEvent): self.issues.append(event) elif label == "pr" and isinstance(event, RawEventsEvent): self.prs.append(event) elif label == "review" and isinstance(event, RawEventsEvent): self.reviews.append(event) elif label == "repo" and isinstance(event, RawEventsEvent): self.repos.append(event) def get_user_all_contribution_events( user_id: str, start_date: datetime, end_date: datetime, access_token: Optional[str] = None, ) -> Dict[str, ContribsList]: repo_contribs: Dict[str, ContribsList] = defaultdict(lambda: ContribsList()) after: Optional[str] = "" cont = True while cont: after_str = after if isinstance(after, str) else "" response = get_user_contribution_events( user_id=user_id, start_date=start_date, end_date=end_date, after=after_str, access_token=access_token, ) cont = False node_lists = [ ("commit", response.commit_contribs_by_repo), ("issue", response.issue_contribs_by_repo), ("pr", response.pr_contribs_by_repo), ("review", response.review_contribs_by_repo), ] for event_type, event_list in node_lists: for repo in event_list: name = repo.repo.name for event in repo.contribs.nodes: repo_contribs[name].add(event_type, event) if repo.contribs.page_info.has_next_page: after = repo.contribs.page_info.end_cursor cont = True for repo in response.repo_contribs.nodes: name = repo.repo.name node = RawEventsEvent(occurredAt=repo.occurred_at) repo_contribs[name].add("repo", node) return repo_contribs def get_all_commit_info( user_id: str, name_with_owner: str, start_date: datetime, end_date: datetime, access_token: Optional[str] = None, ) -> List[RESTRawCommit]: owner, repo = name_with_owner.split("/") data: List[RESTRawCommit] = [] for i in range(10): if len(data) == 100 * i: new_data = get_repo_commits( owner, repo, user_id, start_date, end_date, i + 1, access_token ) data.extend(new_data) # sort ascending return sorted(data, key=lambda x: x.timestamp) async def get_all_commit_languages( commit_infos: List[List[RESTRawCommit]], repos: List[str], repo_infos: Dict[str, RawRepo], access_token: Optional[str] = None, catch_errors: bool = False, ) -> Tuple[Dict[str, List[datetime]], Dict[str, List[CommitLanguages]]]: commit_node_ids = [[x.node_id for x in repo] for repo in commit_infos] commit_times = [[x.timestamp for x in repo] for repo in commit_infos] id_mapping: Dict[str, Tuple[int, int]] = {} repo_mapping: Dict[str, str] = {} all_node_ids: List[str] = [] for i, repo_node_ids in enumerate(commit_node_ids): for j, node_id in enumerate(repo_node_ids): id_mapping[node_id] = (i, j) repo_mapping[node_id] = repos[i] all_node_ids.append(node_id) node_id_chunks: List[List[str]] = [ all_node_ids[i : min(len(all_node_ids), i + GRAPHQL_NODE_CHUNK_SIZE)] for i in range(0, len(all_node_ids), GRAPHQL_NODE_CHUNK_SIZE) ] commit_language_chunks: List[List[Optional[GraphQLRawCommit]]] = await gather( funcs=[get_commits for _ in node_id_chunks], args_dicts=[ { "node_ids": node_id_chunk, "access_token": access_token, "catch_errors": catch_errors, } for node_id_chunk in node_id_chunks ], max_threads=GRAPHQL_NODE_THREADS, ) temp_commit_languages: List[Optional[GraphQLRawCommit]] = [] for commit_language_chunk in commit_language_chunks: temp_commit_languages.extend(commit_language_chunk) # returns commits with no associated PR or incomplete PR filtered_commits: List[GraphQLRawCommit] = filter( lambda x: x is not None and (len(x.prs.nodes) == 0 or x.prs.nodes[0].changed_files > PR_FILES) and (x.additions + x.deletions > 100), temp_commit_languages, ) # type: ignore # get NODE_QUERIES largest commits with no associated PR or incomplete PR sorted_commits = sorted( filtered_commits, key=lambda x: x.additions + x.deletions, reverse=True )[:NODE_QUERIES] sorted_commit_urls = [commit.url.split("/") for commit in sorted_commits] commit_files: List[List[RawCommitFile]] = await gather( funcs=[get_commit_files for _ in sorted_commit_urls], args_dicts=[ { "owner": url[3], "repo": url[4], "sha": url[6], "access_token": access_token, } for url in sorted_commit_urls ], max_threads=REST_NODE_THREADS, ) commit_files_dict: Dict[str, List[RawCommitFile]] = { commit.url: commit_file for commit, commit_file in zip(sorted_commits, commit_files) } commit_languages: List[List[CommitLanguages]] = [ [CommitLanguages() for _ in repo] for repo in commit_infos ] for raw_commits, node_ids in zip(commit_language_chunks, node_id_chunks): for raw_commit, node_id in zip(raw_commits, node_ids): curr_commit_files: Optional[List[RawCommitFile]] = None if raw_commit is not None and raw_commit.url in commit_files_dict: curr_commit_files = commit_files_dict[raw_commit.url] lang_breakdown = get_commit_languages( raw_commit, curr_commit_files, repo_infos[repo_mapping[node_id]] ) i, j = id_mapping[node_id] commit_languages[i][j] = lang_breakdown commit_times_dict: Dict[str, List[datetime]] = {} commit_languages_dict: Dict[str, List[CommitLanguages]] = {} for repo, times, languages in zip(repos, commit_times, commit_languages): commit_times_dict[repo] = times commit_languages_dict[repo] = languages return commit_times_dict, commit_languages_dict async def get_cleaned_contributions( user_id: str, start_date: datetime, end_date: datetime, access_token: Optional[str], catch_errors: bool = False, ) -> Tuple[ RawCalendar, Dict[str, ContribsList], Dict[str, RawRepo], Dict[str, List[datetime]], Dict[str, List[CommitLanguages]], ]: calendar = get_user_contribution_calendar( user_id, start_date, end_date, access_token ) contrib_events = get_user_all_contribution_events( user_id, start_date, end_date, access_token ) repos: List[str] = list(set(contrib_events.keys())) commit_infos: List[List[RESTRawCommit]] = await gather( funcs=[get_all_commit_info for _ in repos], args_dicts=[ { "user_id": user_id, "name_with_owner": repo, "start_date": start_date, "end_date": end_date, "access_token": access_token, } for repo in repos ], max_threads=REST_NODE_THREADS, ) _repo_infos: List[Optional[RawRepo]] = await gather( funcs=[get_repo for _ in repos], args_dicts=[ { "owner": repo.split("/")[0], "repo": repo.split("/")[1], "access_token": access_token, "catch_errors": catch_errors, } for repo in repos ], max_threads=REST_NODE_THREADS, ) repo_infos = {k: v for k, v in zip(repos, _repo_infos) if v is not None} commit_times_dict, commit_languages_dict = await get_all_commit_languages( commit_infos, repos, repo_infos, access_token, catch_errors, ) return ( calendar, contrib_events, repo_infos, commit_times_dict, commit_languages_dict, ) class StatsContainer: def __init__(self): self.contribs: int = 0 self.commits: int = 0 self.issues: int = 0 self.prs: int = 0 self.reviews: int = 0 self.repos: int = 0 self.other: int = 0 self.languages = CommitLanguages() def add_stat(self, label: str, count: int, add: bool = False) -> None: if label == "commit": self.commits += count elif label == "issue": self.issues += count elif label == "pr": self.prs += count elif label == "review": self.reviews += count elif label == "repo": self.repos += count if add: self.contribs += count else: self.other -= count def to_dict(self) -> Dict[str, Any]: return { "contribs_count": self.contribs, "commits_count": self.commits, "issues_count": self.issues, "prs_count": self.prs, "reviews_count": self.reviews, "repos_count": self.repos, "other_count": self.other, "languages": self.languages.to_dict(), } class ListsContainer: def __init__(self): self.commits: List[datetime] = [] self.issues: List[datetime] = [] self.prs: List[datetime] = [] self.reviews: List[datetime] = [] self.repos: List[datetime] = [] def add_list(self, label: str, times: List[datetime]) -> None: if label == "commit": self.commits.extend(times) elif label == "issue": self.issues.extend(times) elif label == "pr": self.prs.extend(times) elif label == "review": self.reviews.extend(times) elif label == "repo": self.repos.extend(times) def to_dict(self) -> Dict[str, Any]: return { "commits": self.commits, "issues": self.issues, "prs": self.prs, "reviews": self.reviews, "repos": self.repos, } class DateContainer: def __init__(self): self.date = "" self.weekday = 0 self.stats = StatsContainer() self.lists = ListsContainer() def add_stat( self, label: str, count: int, times: List[datetime], add: bool = False ): self.stats.add_stat(label, count, add) self.lists.add_list(label, times) def to_dict(self) -> Dict[str, Any]: return { "date": self.date, "weekday": self.weekday, "stats": self.stats.to_dict(), "lists": self.lists.to_dict(), } # assumed one month span, can be no more than one year async def get_contributions( user_id: str, start_date: date, end_date: date, timezone_str: str = "US/Eastern", access_token: Optional[str] = None, catch_errors: bool = False, ) -> UserContributions: tz = pytz.timezone(timezone_str) start_month = date_to_datetime(start_date) end_month = date_to_datetime(end_date, hour=23, minute=59, second=59) ( calendar, contrib_events, repo_infos, commit_times_dict, commit_languages_dict, ) = await get_cleaned_contributions( user_id, start_month, end_month, access_token, catch_errors ) total_stats = StatsContainer() public_stats = StatsContainer() total: Dict[str, DateContainer] = defaultdict(DateContainer) public: Dict[str, DateContainer] = defaultdict(DateContainer) repo_stats: Dict[str, StatsContainer] = defaultdict(StatsContainer) repositories: Dict[str, Dict[str, DateContainer]] = defaultdict( lambda: defaultdict(DateContainer) ) for week in calendar.weeks: for day in week.contribution_days: day_str = str(day.date) for obj, stats_obj in [(total, total_stats), (public, public_stats)]: obj[day_str].date = day.date.isoformat() obj[day_str].weekday = day.weekday obj[day_str].stats.contribs = day.count obj[day_str].stats.other = day.count stats_obj.contribs += day.count stats_obj.other += day.count def update_stats( date_str: str, repo: str, event: str, count: int, times: List[datetime] ): # update global counts for this event total[date_str].add_stat(event, count, times) total_stats.add_stat(event, count) if not repo_infos[repo].is_private: public[date_str].add_stat(event, count, times) public_stats.add_stat(event, count) repositories[repo][date_str].add_stat(event, count, times, add=True) repo_stats[repo].add_stat(event, count, add=True) def update_langs(date_str: str, repo: str, langs: CommitLanguages): stores = [ total[date_str].stats.languages, total_stats.languages, repositories[repo][date_str].stats.languages, repo_stats[repo].languages, ] if not repo_infos[repo].is_private: stores.append(public[date_str].stats.languages) stores.append(public_stats.languages) for store in stores: store += langs for repo, repo_events in contrib_events.items(): for label, events in [ ("commit", repo_events.commits), ("issue", repo_events.issues), ("pr", repo_events.prs), ("review", repo_events.reviews), ("repo", repo_events.repos), ]: events = sorted(events, key=lambda x: x.occurred_at) for event in events: datetime_obj = event.occurred_at.astimezone(tz) date_str = datetime_obj.date().isoformat() repositories[repo][date_str].date = date_str if isinstance(event, RawEventsCommit): count = 0 commit_times: List[datetime] = [] while len(commit_languages_dict[repo]) > 0 and count < event.count: commit_times.append(commit_times_dict[repo].pop(0)) langs = commit_languages_dict[repo].pop(0) update_langs(date_str, repo, langs) count += 1 update_stats(date_str, repo, "commit", event.count, commit_times) else: update_stats(date_str, repo, label, 1, [datetime_obj]) total_stats_dict = total_stats.to_dict() public_stats_dict = public_stats.to_dict() repo_stats_dict = {name: stats.to_dict() for name, stats in repo_stats.items()} for repo in repo_stats: repo_stats_dict[repo]["private"] = repo_infos[repo].is_private total_list = [v.to_dict() for v in total.values() if v.stats.contribs > 0] public_list = [v.to_dict() for v in public.values() if v.stats.contribs > 0] repositories_list = { name: [v.to_dict() for v in repo.values()] for name, repo in repositories.items() } output = UserContributions.model_validate( { "total_stats": total_stats_dict, "public_stats": public_stats_dict, "total": total_list, "public": public_list, "repo_stats": repo_stats_dict, "repos": repositories_list, } ) return output ================================================ FILE: backend/src/aggregation/layer0/follows.py ================================================ from typing import List, Optional from src.data.github.graphql import ( get_user_followers as _get_user_followers, get_user_following as _get_user_following, ) from src.models import User, UserFollows def get_user_follows(user_id: str, access_token: Optional[str]) -> UserFollows: """get user followers and users following for given user""" followers: List[User] = [] following: List[User] = [] for user_list, get_func in zip( [followers, following], [_get_user_followers, _get_user_following] ): after: Optional[str] = "" index, cont = 0, True # initialize variables while cont and index < 10: after_str: str = after if isinstance(after, str) else "" data = get_func(user_id, after=after_str, access_token=access_token) cont = False user_list.extend( map( lambda x: User(name=x.name, login=x.login, url=x.url), data.nodes, ) ) if data.page_info.has_next_page: after = data.page_info.end_cursor cont = True index += 1 return UserFollows(followers=followers, following=following) ================================================ FILE: backend/src/aggregation/layer0/languages.py ================================================ from json import load from typing import Any, Dict, List, Optional, Union from src.constants import BLACKLIST, CUTOFF, DEFAULT_COLOR, FILE_CUTOFF from src.data.github.graphql import RawCommit, RawRepo from src.data.github.rest import RawCommitFile EXTENSIONS: Dict[str, Dict[str, str]] = load(open("./src/data/github/extensions.json")) class CommitLanguages: def __init__(self): self.langs: Dict[str, Dict[str, Union[str, int]]] = {} def __repr__(self): return str(self.langs) def add_lines( self, name: str, color: Optional[str], additions: int, deletions: int ): if ( name not in BLACKLIST and max(additions, deletions) > 0 and max(additions, deletions) < FILE_CUTOFF ): color = color or DEFAULT_COLOR if name not in self.langs: self.langs[name] = {"color": color, "additions": 0, "deletions": 0} self.langs[name]["additions"] += additions # type: ignore self.langs[name]["deletions"] += deletions # type: ignore def normalize(self, add_ratio: float, del_ratio: float): for lang in self.langs: new_add = round(int(self.langs[lang]["additions"]) * add_ratio) self.langs[lang]["additions"] = new_add new_del = round(int(self.langs[lang]["deletions"]) * del_ratio) self.langs[lang]["deletions"] = new_del def __add__(self, other: "CommitLanguages"): for lang in other.langs: if lang not in self.langs: self.langs[lang] = other.langs[lang].copy() else: self.langs[lang]["additions"] += other.langs[lang]["additions"] # type: ignore self.langs[lang]["deletions"] += other.langs[lang]["deletions"] # type: ignore def to_dict(self) -> Dict[str, Any]: return self.langs def get_commit_languages( commit: Optional[RawCommit], files: Optional[List[RawCommitFile]], repo: RawRepo, ) -> CommitLanguages: out = CommitLanguages() if commit is None: return out if max(commit.additions, commit.deletions) == 0: return out # assummed to be auto-generated or copied if max(commit.additions, commit.deletions) > 10 * CUTOFF or ( max(commit.additions, commit.deletions) > CUTOFF and min(commit.additions, commit.deletions) == 0 ): return out pr_coverage = 0 if len(commit.prs.nodes) > 0: pr_obj = commit.prs.nodes[0] pr_files = pr_obj.files.nodes total_changed = sum(x.additions + x.deletions for x in pr_files) pr_coverage = total_changed / max(1, (pr_obj.additions + pr_obj.deletions)) if files is not None: for file in files: filename = file.filename.split(".") extension = "" if len(filename) <= 1 else filename[-1] lang = EXTENSIONS.get(f".{extension}", None) if lang is not None: out.add_lines( lang["name"], lang["color"], file.additions, file.deletions ) elif len(commit.prs.nodes) > 0 and pr_coverage > 0.25: pr = commit.prs.nodes[0] total_additions, total_deletions = 0, 0 for file in pr.files.nodes: filename = file.path.split(".") extension = "" if len(filename) <= 1 else filename[-1] lang = EXTENSIONS.get(f".{extension}", None) if lang is not None: out.add_lines( lang["name"], lang["color"], file.additions, file.deletions ) total_additions += file.additions total_deletions += file.deletions add_ratio = min(pr.additions, commit.additions) / max(1, total_additions) del_ratio = min(pr.deletions, commit.deletions) / max(1, total_deletions) out.normalize(add_ratio, del_ratio) elif commit.additions + commit.deletions > 2 * CUTOFF: # assummed to be auto generated return out else: repo_info = repo.languages.edges languages = [x for x in repo_info if x.node.name not in BLACKLIST] total_repo_size = sum(language.size for language in languages) for language in languages: lang_name = language.node.name lang_color = language.node.color lang_size = language.size additions = round(commit.additions * lang_size / total_repo_size) deletions = round(commit.deletions * lang_size / total_repo_size) out.add_lines(lang_name, lang_color, additions, deletions) return out ================================================ FILE: backend/src/aggregation/layer0/package.py ================================================ from datetime import date from typing import Optional from src.aggregation.layer0.contributions import get_contributions from src.models import UserPackage # from src.subscriber.aggregation.user.follows import get_user_follows async def get_user_data( user_id: str, start_date: date, end_date: date, timezone_str: str, access_token: Optional[str], catch_errors: bool = False, ) -> UserPackage: """packages all processing steps for the user query""" contribs = await get_contributions( user_id=user_id, start_date=start_date, end_date=end_date, timezone_str=timezone_str, access_token=access_token, catch_errors=catch_errors, ) return UserPackage(contribs=contribs) ================================================ FILE: backend/src/aggregation/layer1/__init__.py ================================================ from src.aggregation.layer1.user import query_user __all__ = ["query_user"] ================================================ FILE: backend/src/aggregation/layer1/auth.py ================================================ from datetime import timedelta from typing import List, Optional, Tuple from src.constants import OWNER, REPO from src.data.github.rest import ( RESTError, RESTErrorNotFound, get_repo_stargazers as github_get_repo_stargazers, get_user as github_get_user, get_user_starred_repos as github_get_user_starred_repos, ) from src.data.github.utils import get_access_token from src.data.mongo.user import get_public_user as db_get_public_user from src.utils import alru_cache async def get_valid_github_user(user_id: str) -> Optional[str]: access_token = get_access_token() try: return github_get_user(user_id, access_token)["login"] except (RESTErrorNotFound, KeyError): # User does not exist return None except RESTError: # Rate limited, so assume user exists return user_id async def get_valid_db_user(user_id: str) -> bool: user = await db_get_public_user(user_id) return user is not None @alru_cache(ttl=timedelta(minutes=15)) async def get_repo_stargazers( owner: str = OWNER, repo: str = REPO, no_cache: bool = False ) -> Tuple[bool, List[str]]: access_token = get_access_token() data: List[str] = [] page = 0 while len(data) == 100 * page: temp_data = github_get_repo_stargazers(access_token, owner, repo, page=page) temp_data = [x["user"]["login"] for x in temp_data] data.extend(temp_data) page += 1 return (True, data) async def get_user_stars(user_id: str) -> List[str]: access_token = get_access_token() try: data = github_get_user_starred_repos(user_id, access_token) data = [x["repo"]["full_name"] for x in data] return data except RESTErrorNotFound: # User does not exist (and rate limited previously) return [] except RESTError: # Rate limited, so assume user starred repo return [f"{OWNER}/{REPO}"] ================================================ FILE: backend/src/aggregation/layer1/user.py ================================================ from calendar import monthrange from datetime import date, datetime, timedelta from typing import List, Optional, Tuple import requests from src.aggregation.layer0.package import get_user_data from src.constants import API_VERSION # , BACKEND_URL, PROD from src.data.github.graphql import GraphQLErrorRateLimit from src.data.mongo.secret import update_keys from src.data.mongo.user_months import UserMonth, get_user_months, set_user_month from src.models.user.main import UserPackage from src.utils import alru_cache, date_to_datetime s = requests.Session() # Formerly the subscriber, compute and save new data here async def query_user_month( user_id: str, access_token: Optional[str], private_access: bool, start_date: date, retries: int = 0, ) -> Optional[UserMonth]: year, month = start_date.year, start_date.month end_day = monthrange(year, month)[1] end_date = date(year, month, end_day) try: data = await get_user_data( user_id=user_id, start_date=start_date, end_date=end_date, timezone_str="US/Eastern", access_token=access_token, catch_errors=retries > 0, ) except GraphQLErrorRateLimit: return None except Exception: # Retry, catching exceptions and marking incomplete this time if retries < 1: await query_user_month( user_id, access_token, private_access, start_date, retries + 1 ) return None month_completed = datetime.now() > date_to_datetime(end_date) + timedelta(days=1) user_month = UserMonth.model_validate( { "user_id": user_id, "month": date_to_datetime(start_date), "version": API_VERSION, "private": private_access, "complete": retries == 0 and month_completed, "data": data, } ) await set_user_month(user_month) return user_month @alru_cache(ttl=timedelta(hours=6)) async def query_user( user_id: str, access_token: Optional[str], private_access: bool = False, start_date: date = date.today() - timedelta(365), end_date: date = date.today(), max_time: int = 3600, # seconds no_cache: bool = False, ) -> Tuple[bool, UserPackage]: # Return (possibly incomplete) within max_time seconds start_time = datetime.now() incomplete = False await update_keys() curr_data: List[UserMonth] = await get_user_months( user_id, private_access, start_date, end_date ) curr_months = [x.month for x in curr_data if x.complete] month, year = start_date.month, start_date.year new_months: List[date] = [] while date(year, month, 1) <= end_date: start = date(year, month, 1) if date_to_datetime(start) not in curr_months: new_months.append(start) month = month % 12 + 1 year = year + (month == 1) # Start with complete months and add any incomplete months all_user_packages: List[UserPackage] = [x.data for x in curr_data if x.complete] for month in new_months: if datetime.now() - start_time < timedelta(seconds=max_time): temp = await query_user_month(user_id, access_token, private_access, month) if temp is not None: all_user_packages.append(temp.data) else: incomplete = True else: incomplete = True out: UserPackage = UserPackage.empty() if len(all_user_packages) > 0: out = all_user_packages[0] for user_package in all_user_packages[1:]: out += user_package out.incomplete = incomplete if incomplete or len(new_months) > 1: # TODO: figure out why this causes an infinite loop # # cache buster for publisher # if PROD: # s.get(f"{BACKEND_URL}/user/{user_id}?no_cache=True") return (False, out) # only cache if just the current month updated return (True, out) ================================================ FILE: backend/src/aggregation/layer2/__init__.py ================================================ from src.aggregation.layer2.auth import get_is_valid_user from src.aggregation.layer2.user import get_user, get_user_demo __all__ = ["get_is_valid_user", "get_user", "get_user_demo"] ================================================ FILE: backend/src/aggregation/layer2/auth.py ================================================ from datetime import timedelta from typing import Optional, Tuple from src.aggregation.layer1.auth import ( get_repo_stargazers, get_user_stars, get_valid_db_user, get_valid_github_user, ) from src.constants import OWNER, REPO, USER_BLACKLIST, USER_WHITELIST from src.data.github.rest import RESTError from src.utils import alru_cache async def check_github_user_exists(user_id: str) -> Optional[str]: return await get_valid_github_user(user_id) async def check_db_user_exists(user_id: str) -> bool: return await get_valid_db_user(user_id) async def check_user_starred_repo( user_id: str, owner: str = OWNER, repo: str = REPO ) -> bool: # Checks the repo's starred users (with cache) try: repo_stargazers = await get_repo_stargazers(owner, repo) if user_id in repo_stargazers: return True except RESTError: return True # Assume the user has starred the repo # Checks the user's 30 most recent starred repos (no cache) user_stars = await get_user_stars(user_id) return f"{owner}/{repo}" in user_stars @alru_cache(ttl=timedelta(hours=1)) async def get_is_valid_user(user_id: str) -> Tuple[bool, str]: if user_id.lower() in USER_BLACKLIST: # TODO: change error message return (False, "GitHub user not found") if user_id.lower() in USER_WHITELIST: return (True, f"Valid user {user_id.lower()}") valid_user_id = await check_github_user_exists(user_id) if valid_user_id is None: return (False, "GitHub user not found") valid_db_user = await check_db_user_exists(valid_user_id) user_starred = await check_user_starred_repo(valid_user_id) if not (user_starred or valid_db_user): return (False, "Repo not starred") return (True, f"Valid user {valid_user_id}") ================================================ FILE: backend/src/aggregation/layer2/user.py ================================================ from datetime import date, timedelta from typing import Optional, Tuple from src.aggregation.layer0 import get_user_data from src.constants import USER_BLACKLIST from src.data.mongo.secret.functions import update_keys from src.data.mongo.user import PublicUserModel, get_public_user as db_get_public_user from src.data.mongo.user_months import get_user_months from src.models import UserPackage from src.models.background import UpdateUserBackgroundTask from src.utils import alru_cache # Formerly the publisher, loads existing data here async def _get_user( user_id: str, private_access: bool, start_date: date, end_date: date ) -> Tuple[Optional[UserPackage], bool]: user_months = await get_user_months(user_id, private_access, start_date, end_date) if len(user_months) == 0: return None, False expected_num_months = ( (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1 ) complete = len(user_months) == expected_num_months user_data = user_months[0].data for user_month in user_months[1:]: user_data += user_month.data # TODO: handle timezone_str here return user_data.trim(start_date, end_date), complete @alru_cache() async def get_user( user_id: str, start_date: date, end_date: date, no_cache: bool = False, ) -> Tuple[ bool, Tuple[Optional[UserPackage], bool, Optional[UpdateUserBackgroundTask]] ]: if user_id in USER_BLACKLIST: return (False, (None, False, None)) user: Optional[PublicUserModel] = await db_get_public_user(user_id) if user is None: return (False, (None, False, None)) private_access = user.private_access or False user_data, complete = await _get_user(user_id, private_access, start_date, end_date) background_task = UpdateUserBackgroundTask( user_id=user_id, access_token=user.access_token, private_access=private_access, start_date=start_date, end_date=end_date, ) return (complete, (user_data, complete, background_task)) @alru_cache(ttl=timedelta(minutes=15)) async def get_user_demo( user_id: str, start_date: date, end_date: date, no_cache: bool = False ) -> Tuple[bool, UserPackage]: await update_keys() timezone_str = "US/Eastern" # recompute/cache but don't save to db data = await get_user_data( user_id=user_id, start_date=start_date, end_date=end_date, timezone_str=timezone_str, access_token=None, catch_errors=True, ) return (True, data) ================================================ FILE: backend/src/constants.py ================================================ import os # GLOBAL LOCAL = os.getenv("LOCAL", "False") == "True" PROD = os.getenv("PROD", "False") == "True" PROJECT_ID = "github-334619" BACKEND_URL = "https://api.githubtrends.io" if PROD else "http://localhost:8000" OWNER = "avgupta456" REPO = "github-trends" # API # https://docs.github.com/en/rest/reference/rate-limit # https://docs.github.com/en/rest/guides/best-practices-for-integrators#dealing-with-secondary-rate-limits # https://docs.github.com/en/graphql/overview/resource-limitations TIMEOUT = 15 # max seconds to wait for api response GRAPHQL_NODE_CHUNK_SIZE = 50 # number of nodes (commits) to query (max 100) GRAPHQL_NODE_THREADS = 5 # number of node queries simultaneously (avoid blacklisting) REST_NODE_THREADS = 50 # number of node queries simultaneously (avoid blacklisting) PR_FILES = 5 # max number of files to query for PRs NODE_QUERIES = 20 # max number of node queries to make CUTOFF = 1000 # if additions or deletions > CUTOFF, or sum > 2 * CUTOFF, ignore LOC FILE_CUTOFF = 1000 # if less than cutoff in file, count LOC API_VERSION = 0.02 # determines when to overwrite MongoDB data # CUSTOMIZATION BLACKLIST = ["Jupyter Notebook", "HTML"] # languages to ignore # OAUTH prefix = "PROD" if PROD else "DEV" # client ID for GitHub OAuth App OAUTH_CLIENT_ID = os.getenv(f"{prefix}_OAUTH_CLIENT_ID", "") # client secret for App OAUTH_CLIENT_SECRET = os.getenv(f"{prefix}_OAUTH_CLIENT_SECRET", "") # redirect uri for App OAUTH_REDIRECT_URI = os.getenv(f"{prefix}_OAUTH_REDIRECT_URI", "") # MONGODB MONGODB_PASSWORD = os.getenv("MONGODB_PASSWORD", "") # SVG DEFAULT_COLOR = "#858585" # SENTRY SENTRY_DSN = os.getenv("SENTRY_DSN", "") # TESTING TEST_USER_ID = "avgupta456" TEST_REPO = "github-trends" TEST_TOKEN = os.getenv("AUTH_TOKEN", "") # for authentication TEST_NODE_IDS = [ "C_kwDOENp939oAKGM1MzdlM2QzMTZjMmEyZGIyYWU4ZWI0MmNmNjQ4YWEwNWQ5OTBiMjM", "C_kwDOD_-BVNoAKDFhNTIxNWE1MGM4ZDllOGEwYTFhNjhmYWZkYzE5MzA5YTRkMDMwZmM", "C_kwDOD_-BVNoAKDRiZTQ4MTQ0MzgwYjBlNGEwNjQ4YjY4YWI4ZjFjYmQ3MWU4M2VhMzU", ] TEST_SHA = "ad83e6340377904fa0295745b5314202b23d2f3f" # WRAPPED # example users, don't need to star the repo USER_WHITELIST = [ "torvalds", "yyx990803", "shadcn", "sindresorhus", ] USER_BLACKLIST = ["kangmingtay", "ae7er", "stalukdar7", "piyush7833"] print("PROD", PROD) print("API_VERSION", API_VERSION) print() ================================================ FILE: backend/src/data/__init__.py ================================================ ================================================ FILE: backend/src/data/github/__init__.py ================================================ ================================================ FILE: backend/src/data/github/auth/__init__.py ================================================ from src.data.github.auth.main import authenticate __all__ = ["authenticate"] ================================================ FILE: backend/src/data/github/auth/main.py ================================================ from datetime import datetime from typing import Dict, Optional, Tuple import requests from src.constants import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI s = requests.session() def get_unknown_user(access_token: str) -> Optional[str]: """ Accepts access_token and returns user_id of associated user :param access_token: GitHub access token :return: user_id or None if invalid access_token """ headers: Dict[str, str] = { "Accept": "application/vnd.github.v3+json", "Authorization": f"bearer {access_token}", } r = s.get("https://api.github.com/user", params={}, headers=headers) return r.json().get("login", None) class OAuthError(Exception): pass async def authenticate(code: str) -> Tuple[str, str]: """ Takes a authentication code, verifies, and returns user_id/access_token :param code: GitHub authentication code from OAuth process :return: user_id, access_token of authenticated user """ start = datetime.now() params = { "client_id": OAUTH_CLIENT_ID, "client_secret": OAUTH_CLIENT_SECRET, "code": code, "redirect_uri": OAUTH_REDIRECT_URI, } r = s.post("https://github.com/login/oauth/access_token", params=params) if r.status_code != 200: raise OAuthError(f"OAuth Error: {str(r.status_code)}") access_token = r.text.split("&")[0].split("=")[1] user_id = get_unknown_user(access_token) if user_id is None: raise OAuthError("OAuth Error: Invalid user_id/access_token") print("OAuth SignUp", datetime.now() - start) return user_id, access_token ================================================ FILE: backend/src/data/github/extensions.json ================================================ { ".1": { "color": "#ecdebe", "name": "Roff Manpage" }, ".1in": { "color": "#ecdebe", "name": "Roff Manpage" }, ".1m": { "color": "#ecdebe", "name": "Roff Manpage" }, ".1x": { "color": "#ecdebe", "name": "Roff Manpage" }, ".2": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3in": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3m": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3p": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3pm": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3qt": { "color": "#ecdebe", "name": "Roff Manpage" }, ".3x": { "color": "#ecdebe", "name": "Roff Manpage" }, ".4": { "color": "#ecdebe", "name": "Roff Manpage" }, ".4dm": { "color": "#004289", "name": "4D" }, ".4th": { "color": "#341708", "name": "Forth" }, ".5": { "color": "#ecdebe", "name": "Roff Manpage" }, ".6": { "color": "#ecdebe", "name": "Roff Manpage" }, ".6pl": { "color": "#0000fb", "name": "Raku" }, ".6pm": { "color": "#0000fb", "name": "Raku" }, ".7": { "color": "#ecdebe", "name": "Roff Manpage" }, ".8": { "color": "#ecdebe", "name": "Roff Manpage" }, ".8xk": { "color": "#A0AA87", "name": "TI Program" }, ".8xk.txt": { "color": "#A0AA87", "name": "TI Program" }, ".8xp": { "color": "#A0AA87", "name": "TI Program" }, ".8xp.txt": { "color": "#A0AA87", "name": "TI Program" }, ".9": { "color": "#ecdebe", "name": "Roff Manpage" }, ".E": { "color": "#ccce35", "name": "E" }, "._coffee": { "color": "#244776", "name": "CoffeeScript" }, "._js": { "color": "#f1e05a", "name": "JavaScript" }, "._ls": { "color": "#499886", "name": "LiveScript" }, ".a51": { "color": "#6E4C13", "name": "Assembly" }, ".abap": { "color": "#E8274B", "name": "ABAP" }, ".ada": { "color": "#02f88c", "name": "Ada" }, ".adb": { "color": "#02f88c", "name": "Ada" }, ".ado": { "color": "#1a5f91", "name": "Stata" }, ".adp": { "color": "#e4cc98", "name": "Tcl" }, ".ads": { "color": "#02f88c", "name": "Ada" }, ".agc": { "color": "#0B3D91", "name": "Apollo Guidance Computer" }, ".agda": { "color": "#315665", "name": "Agda" }, ".ahk": { "color": "#6594b9", "name": "AutoHotkey" }, ".ahkl": { "color": "#6594b9", "name": "AutoHotkey" }, ".aidl": { "color": "#34EB6B", "name": "AIDL" }, ".aj": { "color": "#a957b0", "name": "AspectJ" }, ".al": { "color": "#0298c3", "name": "Perl" }, ".als": { "color": "#64C800", "name": "Alloy" }, ".ampl": { "color": "#E6EFBB", "name": "AMPL" }, ".angelscript": { "color": "#C7D7DC", "name": "AngelScript" }, ".apib": { "color": "#2ACCA8", "name": "API Blueprint" }, ".apl": { "color": "#5A8164", "name": "APL" }, ".app.src": { "color": "#B83998", "name": "Erlang" }, ".applescript": { "color": "#101F1F", "name": "AppleScript" }, ".arc": { "color": "#aa2afe", "name": "Arc" }, ".as": { "color": "#C7D7DC", "name": "AngelScript" }, ".asax": { "color": "#9400ff", "name": "ASP.NET" }, ".asc": { "color": "#B9D9FF", "name": "AGS Script" }, ".ascx": { "color": "#9400ff", "name": "ASP.NET" }, ".asd": { "color": "#3fb68b", "name": "Common Lisp" }, ".asddls": { "color": "#555e25", "name": "ABAP CDS" }, ".ash": { "color": "#B9D9FF", "name": "AGS Script" }, ".ashx": { "color": "#9400ff", "name": "ASP.NET" }, ".asm": { "color": "#005daa", "name": "Motorola 68K Assembly" }, ".asmx": { "color": "#9400ff", "name": "ASP.NET" }, ".asp": { "color": "#6a40fd", "name": "Classic ASP" }, ".aspx": { "color": "#9400ff", "name": "ASP.NET" }, ".astro": { "color": "#ff5a03", "name": "Astro" }, ".asy": { "color": "#ff0000", "name": "Asymptote" }, ".au3": { "color": "#1C3552", "name": "AutoIt" }, ".aug": { "color": "#9CC134", "name": "Augeas" }, ".auk": { "color": "#c30e9b", "name": "Awk" }, ".aux": { "color": "#3D6117", "name": "TeX" }, ".aw": { "color": "#4F5D95", "name": "PHP" }, ".awk": { "color": "#c30e9b", "name": "Awk" }, ".axd": { "color": "#9400ff", "name": "ASP.NET" }, ".axi": { "color": "#0aa0ff", "name": "NetLinx" }, ".axi.erb": { "color": "#747faa", "name": "NetLinx+ERB" }, ".axs": { "color": "#0aa0ff", "name": "NetLinx" }, ".axs.erb": { "color": "#747faa", "name": "NetLinx+ERB" }, ".b": { "color": "#2F2530", "name": "Brainfuck" }, ".bal": { "color": "#FF5000", "name": "Ballerina" }, ".bas": { "color": "#867db1", "name": "VBA" }, ".bash": { "color": "#89e051", "name": "Shell" }, ".bat": { "color": "#C1F12E", "name": "Batchfile" }, ".bats": { "color": "#89e051", "name": "Shell" }, ".bb": { "color": "#00FFAE", "name": "BlitzBasic" }, ".bbx": { "color": "#3D6117", "name": "TeX" }, ".bdy": { "color": "#dad8d8", "name": "PLSQL" }, ".bf": { "color": "#2F2530", "name": "Brainfuck" }, ".bi": { "color": "#867db1", "name": "FreeBasic" }, ".bib": { "color": "#778899", "name": "BibTeX" }, ".bibtex": { "color": "#778899", "name": "BibTeX" }, ".bicep": { "color": "#519aba", "name": "Bicep" }, ".bison": { "color": "#6A463F", "name": "Bison" }, ".blade": { "color": "#f7523f", "name": "Blade" }, ".blade.php": { "color": "#f7523f", "name": "Blade" }, ".bmx": { "color": "#cd6400", "name": "BlitzMax" }, ".bones": { "color": "#f1e05a", "name": "JavaScript" }, ".boo": { "color": "#d4bec1", "name": "Boo" }, ".boot": { "color": "#db5855", "name": "Clojure" }, ".bpl": { "color": "#c80fa0", "name": "Boogie" }, ".brs": { "color": "#662D91", "name": "Brightscript" }, ".bsl": { "color": "#814CCC", "name": "1C Enterprise" }, ".bsv": { "color": "#12223c", "name": "Bluespec" }, ".builder": { "color": "#701516", "name": "Ruby" }, ".bzl": { "color": "#76d275", "name": "Starlark" }, ".c": { "color": "#555555", "name": "C" }, ".c++": { "color": "#f34b7d", "name": "C++" }, ".cake": { "color": "#244776", "name": "CoffeeScript" }, ".capnp": { "color": "#c42727", "name": "Cap'n Proto" }, ".cats": { "color": "#555555", "name": "C" }, ".cbx": { "color": "#3D6117", "name": "TeX" }, ".cc": { "color": "#f34b7d", "name": "C++" }, ".cdf": { "color": "#dd1100", "name": "Mathematica" }, ".ceylon": { "color": "#dfa535", "name": "Ceylon" }, ".cfc": { "color": "#ed2cd6", "name": "ColdFusion CFC" }, ".cfm": { "color": "#ed2cd6", "name": "ColdFusion" }, ".cfml": { "color": "#ed2cd6", "name": "ColdFusion" }, ".cgi": { "color": "#89e051", "name": "Shell" }, ".cginc": { "color": "#aace60", "name": "HLSL" }, ".ch": { "color": "#403a40", "name": "xBase" }, ".chpl": { "color": "#8dc63f", "name": "Chapel" }, ".cirru": { "color": "#ccccff", "name": "Cirru" }, ".cjs": { "color": "#f1e05a", "name": "JavaScript" }, ".cjsx": { "color": "#244776", "name": "CoffeeScript" }, ".ck": { "color": "#3f8000", "name": "ChucK" }, ".cl": { "color": "#ed2e2d", "name": "OpenCL" }, ".cl2": { "color": "#db5855", "name": "Clojure" }, ".click": { "color": "#E4E6F3", "name": "Click" }, ".clj": { "color": "#db5855", "name": "Clojure" }, ".cljc": { "color": "#db5855", "name": "Clojure" }, ".cljs": { "color": "#db5855", "name": "Clojure" }, ".cljs.hl": { "color": "#db5855", "name": "Clojure" }, ".cljscm": { "color": "#db5855", "name": "Clojure" }, ".cljx": { "color": "#db5855", "name": "Clojure" }, ".clp": { "color": "#00A300", "name": "CLIPS" }, ".cls": { "color": "#867db1", "name": "VBA" }, ".clw": { "color": "#db901e", "name": "Clarion" }, ".cmake": { "color": "#DA3434", "name": "CMake" }, ".cmake.in": { "color": "#DA3434", "name": "CMake" }, ".cmd": { "color": "#C1F12E", "name": "Batchfile" }, ".cnc": { "color": "#D08CF2", "name": "G-code" }, ".cocci": { "color": "#c94949", "name": "SmPL" }, ".coffee": { "color": "#244776", "name": "CoffeeScript" }, ".coffee.md": { "color": "#244776", "name": "Literate CoffeeScript" }, ".command": { "color": "#89e051", "name": "Shell" }, ".coq": { "color": "#d0b68c", "name": "Coq" }, ".cp": { "color": "#B0CE4E", "name": "Component Pascal" }, ".cpp": { "color": "#f34b7d", "name": "C++" }, ".cps": { "color": "#B0CE4E", "name": "Component Pascal" }, ".cr": { "color": "#000100", "name": "Crystal" }, ".cs": { "color": "#178600", "name": "C#" }, ".csd": { "color": "#1a1a1a", "name": "Csound Document" }, ".cshtml": { "color": "#512be4", "name": "HTML+Razor" }, ".css": { "color": "#563d7c", "name": "CSS" }, ".csx": { "color": "#178600", "name": "C#" }, ".ctp": { "color": "#4F5D95", "name": "PHP" }, ".cu": { "color": "#3A4E3A", "name": "Cuda" }, ".cue": { "color": "#5886E1", "name": "CUE" }, ".cuh": { "color": "#3A4E3A", "name": "Cuda" }, ".cwl": { "color": "#B5314C", "name": "Common Workflow Language" }, ".cxx": { "color": "#f34b7d", "name": "C++" }, ".d": { "color": "#427819", "name": "Makefile" }, ".dart": { "color": "#00B4AB", "name": "Dart" }, ".dats": { "color": "#1ac620", "name": "ATS" }, ".db2": { "color": "#e38c00", "name": "SQLPL" }, ".dcl": { "color": "#3F85AF", "name": "Clean" }, ".ddl": { "color": "#dad8d8", "name": "PLSQL" }, ".decls": { "color": "#00FFAE", "name": "BlitzBasic" }, ".dfm": { "color": "#E3F171", "name": "Pascal" }, ".dfy": { "color": "#FFEC25", "name": "Dafny" }, ".dhall": { "color": "#dfafff", "name": "Dhall" }, ".di": { "color": "#ba595e", "name": "D" }, ".djs": { "color": "#cca760", "name": "Dogescript" }, ".dlm": { "color": "#a3522f", "name": "IDL" }, ".dm": { "color": "#447265", "name": "DM" }, ".do": { "color": "#1a5f91", "name": "Stata" }, ".dockerfile": { "color": "#384d54", "name": "Dockerfile" }, ".doh": { "color": "#1a5f91", "name": "Stata" }, ".dpr": { "color": "#E3F171", "name": "Pascal" }, ".druby": { "color": "#c7a938", "name": "Mirah" }, ".dsp": { "color": "#c37240", "name": "Faust" }, ".dtx": { "color": "#3D6117", "name": "TeX" }, ".duby": { "color": "#c7a938", "name": "Mirah" }, ".dwl": { "color": "#003a52", "name": "DataWeave" }, ".dyalog": { "color": "#5A8164", "name": "APL" }, ".dyl": { "color": "#6c616e", "name": "Dylan" }, ".dylan": { "color": "#6c616e", "name": "Dylan" }, ".e": { "color": "#4d6977", "name": "Eiffel" }, ".ebuild": { "color": "#9400ff", "name": "Gentoo Ebuild" }, ".ec": { "color": "#913960", "name": "eC" }, ".ecl": { "color": "#001d9d", "name": "ECLiPSe" }, ".eclass": { "color": "#9400ff", "name": "Gentoo Eclass" }, ".eclxml": { "color": "#8a1267", "name": "ECL" }, ".ecr": { "color": "#2e1052", "name": "HTML+ECR" }, ".ect": { "color": "#a91e50", "name": "EJS" }, ".eex": { "color": "#6e4a7e", "name": "HTML+EEX" }, ".eh": { "color": "#913960", "name": "eC" }, ".ejs": { "color": "#a91e50", "name": "EJS" }, ".el": { "color": "#c065db", "name": "Emacs Lisp" }, ".eliom": { "color": "#3be133", "name": "OCaml" }, ".eliomi": { "color": "#3be133", "name": "OCaml" }, ".elm": { "color": "#60B5CC", "name": "Elm" }, ".em": { "color": "#FFF4F3", "name": "EmberScript" }, ".emacs": { "color": "#c065db", "name": "Emacs Lisp" }, ".emacs.desktop": { "color": "#c065db", "name": "Emacs Lisp" }, ".emberscript": { "color": "#FFF4F3", "name": "EmberScript" }, ".env": { "color": "#89e051", "name": "Shell" }, ".eps": { "color": "#da291c", "name": "PostScript" }, ".epsi": { "color": "#da291c", "name": "PostScript" }, ".eq": { "color": "#a78649", "name": "EQ" }, ".erb": { "color": "#701516", "name": "HTML+ERB" }, ".erb.deface": { "color": "#701516", "name": "HTML+ERB" }, ".erl": { "color": "#B83998", "name": "Erlang" }, ".es": { "color": "#f1e05a", "name": "JavaScript" }, ".es6": { "color": "#f1e05a", "name": "JavaScript" }, ".escript": { "color": "#B83998", "name": "Erlang" }, ".ex": { "color": "#6e4a7e", "name": "Elixir" }, ".exs": { "color": "#6e4a7e", "name": "Elixir" }, ".eye": { "color": "#701516", "name": "Ruby" }, ".f": { "color": "#4d41b1", "name": "Fortran" }, ".f03": { "color": "#4d41b1", "name": "Fortran Free Form" }, ".f08": { "color": "#4d41b1", "name": "Fortran Free Form" }, ".f77": { "color": "#4d41b1", "name": "Fortran" }, ".f90": { "color": "#4d41b1", "name": "Fortran Free Form" }, ".f95": { "color": "#4d41b1", "name": "Fortran Free Form" }, ".factor": { "color": "#636746", "name": "Factor" }, ".fan": { "color": "#14253c", "name": "Fantom" }, ".fancypack": { "color": "#7b9db4", "name": "Fancy" }, ".fcgi": { "color": "#89e051", "name": "Shell" }, ".feature": { "color": "#5B2063", "name": "Gherkin" }, ".fish": { "color": "#4aae47", "name": "fish" }, ".flex": { "color": "#DBCA00", "name": "JFlex" }, ".flux": { "color": "#88ccff", "name": "FLUX" }, ".fnc": { "color": "#dad8d8", "name": "PLSQL" }, ".fnl": { "color": "#fff3d7", "name": "Fennel" }, ".for": { "color": "#4d41b1", "name": "Fortran" }, ".forth": { "color": "#341708", "name": "Forth" }, ".fp": { "color": "#5686a5", "name": "GLSL" }, ".fpp": { "color": "#4d41b1", "name": "Fortran" }, ".fr": { "color": "#00cafe", "name": "Frege" }, ".frag": { "color": "#f1e05a", "name": "JavaScript" }, ".frg": { "color": "#5686a5", "name": "GLSL" }, ".frm": { "color": "#867db1", "name": "VBA" }, ".frt": { "color": "#341708", "name": "Forth" }, ".frx": { "color": "#867db1", "name": "VBA" }, ".fs": { "color": "#5686a5", "name": "GLSL" }, ".fsh": { "color": "#5686a5", "name": "GLSL" }, ".fshader": { "color": "#5686a5", "name": "GLSL" }, ".fsi": { "color": "#b845fc", "name": "F#" }, ".fst": { "color": "#572e30", "name": "F*" }, ".fsx": { "color": "#b845fc", "name": "F#" }, ".fth": { "color": "#341708", "name": "Forth" }, ".ftl": { "color": "#0050b2", "name": "FreeMarker" }, ".fun": { "color": "#dc566d", "name": "Standard ML" }, ".fut": { "color": "#5f021f", "name": "Futhark" }, ".fx": { "color": "#aace60", "name": "HLSL" }, ".fxh": { "color": "#aace60", "name": "HLSL" }, ".fy": { "color": "#7b9db4", "name": "Fancy" }, ".g": { "color": "#0000cc", "name": "GAP" }, ".g4": { "color": "#9DC3FF", "name": "ANTLR" }, ".gaml": { "color": "#FFC766", "name": "GAML" }, ".gap": { "color": "#0000cc", "name": "GAP" }, ".gawk": { "color": "#c30e9b", "name": "Awk" }, ".gco": { "color": "#D08CF2", "name": "G-code" }, ".gcode": { "color": "#D08CF2", "name": "G-code" }, ".gd": { "color": "#355570", "name": "GDScript" }, ".gemspec": { "color": "#701516", "name": "Ruby" }, ".geo": { "color": "#5686a5", "name": "GLSL" }, ".geom": { "color": "#5686a5", "name": "GLSL" }, ".gf": { "color": "#ff0000", "name": "Grammatical Framework" }, ".gi": { "color": "#0000cc", "name": "GAP" }, ".glf": { "color": "#c1ac7f", "name": "Glyph" }, ".glsl": { "color": "#5686a5", "name": "GLSL" }, ".glslf": { "color": "#5686a5", "name": "GLSL" }, ".glslv": { "color": "#5686a5", "name": "GLSL" }, ".gml": { "color": "#71b417", "name": "Game Maker Language" }, ".gms": { "color": "#f49a22", "name": "GAMS" }, ".gnu": { "color": "#f0a9f0", "name": "Gnuplot" }, ".gnuplot": { "color": "#f0a9f0", "name": "Gnuplot" }, ".go": { "color": "#00ADD8", "name": "Go" }, ".god": { "color": "#701516", "name": "Ruby" }, ".golo": { "color": "#88562A", "name": "Golo" }, ".gp": { "color": "#f0a9f0", "name": "Gnuplot" }, ".grace": { "color": "#615f8b", "name": "Grace" }, ".groovy": { "color": "#4298b8", "name": "Groovy" }, ".grt": { "color": "#4298b8", "name": "Groovy" }, ".gs": { "color": "#f1e05a", "name": "JavaScript" }, ".gshader": { "color": "#5686a5", "name": "GLSL" }, ".gsp": { "color": "#4298b8", "name": "Groovy Server Pages" }, ".gst": { "color": "#82937f", "name": "Gosu" }, ".gsx": { "color": "#82937f", "name": "Gosu" }, ".gtpl": { "color": "#4298b8", "name": "Groovy" }, ".gvy": { "color": "#4298b8", "name": "Groovy" }, ".gyp": { "color": "#3572A5", "name": "Python" }, ".gypi": { "color": "#3572A5", "name": "Python" }, ".h": { "color": "#438eff", "name": "Objective-C" }, ".h++": { "color": "#f34b7d", "name": "C++" }, ".hack": { "color": "#878787", "name": "Hack" }, ".haml": { "color": "#ece2a9", "name": "Haml" }, ".haml.deface": { "color": "#ece2a9", "name": "Haml" }, ".handlebars": { "color": "#f7931e", "name": "Handlebars" }, ".hats": { "color": "#1ac620", "name": "ATS" }, ".hb": { "color": "#0e60e3", "name": "Harbour" }, ".hbs": { "color": "#f7931e", "name": "Handlebars" }, ".hc": { "color": "#ffefaf", "name": "HolyC" }, ".hh": { "color": "#878787", "name": "Hack" }, ".hhi": { "color": "#878787", "name": "Hack" }, ".hic": { "color": "#db5855", "name": "Clojure" }, ".hlsl": { "color": "#aace60", "name": "HLSL" }, ".hlsli": { "color": "#aace60", "name": "HLSL" }, ".hpp": { "color": "#f34b7d", "name": "C++" }, ".hqf": { "color": "#3F3F3F", "name": "SQF" }, ".hql": { "color": "#dce200", "name": "HiveQL" }, ".hrl": { "color": "#B83998", "name": "Erlang" }, ".hs": { "color": "#5e5086", "name": "Haskell" }, ".hs-boot": { "color": "#5e5086", "name": "Haskell" }, ".hsc": { "color": "#5e5086", "name": "Haskell" }, ".hta": { "color": "#e34c26", "name": "HTML" }, ".htm": { "color": "#e34c26", "name": "HTML" }, ".html": { "color": "#e34c26", "name": "HTML" }, ".html.hl": { "color": "#e34c26", "name": "HTML" }, ".html.leex": { "color": "#6e4a7e", "name": "HTML+EEX" }, ".hx": { "color": "#df7900", "name": "Haxe" }, ".hxsl": { "color": "#df7900", "name": "Haxe" }, ".hxx": { "color": "#f34b7d", "name": "C++" }, ".hy": { "color": "#7790B2", "name": "Hy" }, ".i": { "color": "#005daa", "name": "Motorola 68K Assembly" }, ".i3": { "color": "#223388", "name": "Modula-3" }, ".ice": { "color": "#003fa2", "name": "Slice" }, ".iced": { "color": "#244776", "name": "CoffeeScript" }, ".icl": { "color": "#3F85AF", "name": "Clean" }, ".idc": { "color": "#555555", "name": "C" }, ".idr": { "color": "#b30000", "name": "Idris" }, ".ig": { "color": "#223388", "name": "Modula-3" }, ".ihlp": { "color": "#1a5f91", "name": "Stata" }, ".ijm": { "color": "#99AAFF", "name": "ImageJ Macro" }, ".ijs": { "color": "#9EEDFF", "name": "J" }, ".ik": { "color": "#078193", "name": "Ioke" }, ".ily": { "color": "#9ccc7c", "name": "LilyPond" }, ".inc": { "color": "#f69e1d", "name": "SourcePawn" }, ".inl": { "color": "#f34b7d", "name": "C++" }, ".ino": { "color": "#f34b7d", "name": "C++" }, ".ins": { "color": "#3D6117", "name": "TeX" }, ".intr": { "color": "#6c616e", "name": "Dylan" }, ".io": { "color": "#a9188d", "name": "Io" }, ".iol": { "color": "#843179", "name": "Jolie" }, ".ipf": { "color": "#0000cc", "name": "IGOR Pro" }, ".ipp": { "color": "#f34b7d", "name": "C++" }, ".ipynb": { "color": "#DA5B0B", "name": "Jupyter Notebook" }, ".isl": { "color": "#264b99", "name": "Inno Setup" }, ".iss": { "color": "#264b99", "name": "Inno Setup" }, ".j": { "color": "#ff0c5a", "name": "Objective-J" }, ".j2": { "color": "#a52a22", "name": "Jinja" }, ".jade": { "color": "#a86454", "name": "Pug" }, ".jake": { "color": "#f1e05a", "name": "JavaScript" }, ".jav": { "color": "#b07219", "name": "Java" }, ".java": { "color": "#b07219", "name": "Java" }, ".javascript": { "color": "#f1e05a", "name": "JavaScript" }, ".jbuilder": { "color": "#701516", "name": "Ruby" }, ".jflex": { "color": "#DBCA00", "name": "JFlex" }, ".jinja": { "color": "#a52a22", "name": "Jinja" }, ".jinja2": { "color": "#a52a22", "name": "Jinja" }, ".jison": { "color": "#56b3cb", "name": "Jison" }, ".jisonlex": { "color": "#56b3cb", "name": "Jison Lex" }, ".jl": { "color": "#a270ba", "name": "Julia" }, ".jq": { "color": "#c7254e", "name": "jq" }, ".js": { "color": "#f1e05a", "name": "JavaScript" }, ".js.erb": { "color": "#f1e05a", "name": "JavaScript+ERB" }, ".jsb": { "color": "#f1e05a", "name": "JavaScript" }, ".jscad": { "color": "#f1e05a", "name": "JavaScript" }, ".jsfl": { "color": "#f1e05a", "name": "JavaScript" }, ".jsm": { "color": "#f1e05a", "name": "JavaScript" }, ".jsonnet": { "color": "#0064bd", "name": "Jsonnet" }, ".jsp": { "color": "#2A6277", "name": "Java Server Pages" }, ".jss": { "color": "#f1e05a", "name": "JavaScript" }, ".jst": { "color": "#a91e50", "name": "EJS" }, ".jsx": { "color": "#f1e05a", "name": "JavaScript" }, ".kak": { "color": "#6f8042", "name": "KakouneScript" }, ".kid": { "color": "#951531", "name": "Genshi" }, ".kojo": { "color": "#c22d40", "name": "Scala" }, ".krl": { "color": "#28430A", "name": "KRL" }, ".ksh": { "color": "#89e051", "name": "Shell" }, ".ksy": { "color": "#773b37", "name": "Kaitai Struct" }, ".kt": { "color": "#A97BFF", "name": "Kotlin" }, ".ktm": { "color": "#A97BFF", "name": "Kotlin" }, ".kts": { "color": "#A97BFF", "name": "Kotlin" }, ".l": { "color": "#ecdebe", "name": "Roff" }, ".lagda": { "color": "#315665", "name": "Literate Agda" }, ".las": { "color": "#999999", "name": "Lasso" }, ".lasso": { "color": "#999999", "name": "Lasso" }, ".lasso8": { "color": "#999999", "name": "Lasso" }, ".lasso9": { "color": "#999999", "name": "Lasso" }, ".latte": { "color": "#f2a542", "name": "Latte" }, ".lbx": { "color": "#3D6117", "name": "TeX" }, ".less": { "color": "#1d365d", "name": "Less" }, ".lex": { "color": "#DBCA00", "name": "Lex" }, ".lfe": { "color": "#4C3023", "name": "LFE" }, ".lgt": { "color": "#295b9a", "name": "Logtalk" }, ".lhs": { "color": "#5e5086", "name": "Literate Haskell" }, ".libsonnet": { "color": "#0064bd", "name": "Jsonnet" }, ".lid": { "color": "#6c616e", "name": "Dylan" }, ".lidr": { "color": "#b30000", "name": "Idris" }, ".linq": { "color": "#178600", "name": "C#" }, ".liquid": { "color": "#67b8de", "name": "Liquid" }, ".lisp": { "color": "#87AED7", "name": "NewLisp" }, ".litcoffee": { "color": "#244776", "name": "Literate CoffeeScript" }, ".ll": { "color": "#185619", "name": "LLVM" }, ".lmi": { "color": "#3572A5", "name": "Python" }, ".logtalk": { "color": "#295b9a", "name": "Logtalk" }, ".lol": { "color": "#cc9900", "name": "LOLCODE" }, ".lookml": { "color": "#652B81", "name": "LookML" }, ".lpr": { "color": "#E3F171", "name": "Pascal" }, ".ls": { "color": "#499886", "name": "LiveScript" }, ".lsl": { "color": "#3d9970", "name": "LSL" }, ".lslp": { "color": "#3d9970", "name": "LSL" }, ".lsp": { "color": "#87AED7", "name": "NewLisp" }, ".ltx": { "color": "#3D6117", "name": "TeX" }, ".lua": { "color": "#000080", "name": "Lua" }, ".lvlib": { "color": "#fede06", "name": "LabVIEW" }, ".lvproj": { "color": "#fede06", "name": "LabVIEW" }, ".ly": { "color": "#9ccc7c", "name": "LilyPond" }, ".m": { "color": "#438eff", "name": "Objective-C" }, ".m2": { "color": "#d8ffff", "name": "Macaulay2" }, ".m3": { "color": "#223388", "name": "Modula-3" }, ".ma": { "color": "#dd1100", "name": "Mathematica" }, ".mak": { "color": "#427819", "name": "Makefile" }, ".make": { "color": "#427819", "name": "Makefile" }, ".makefile": { "color": "#427819", "name": "Makefile" }, ".mako": { "color": "#7e858d", "name": "Mako" }, ".man": { "color": "#ecdebe", "name": "Roff Manpage" }, ".mao": { "color": "#7e858d", "name": "Mako" }, ".marko": { "color": "#42bff2", "name": "Marko" }, ".mask": { "color": "#f97732", "name": "Mask" }, ".mata": { "color": "#1a5f91", "name": "Stata" }, ".matah": { "color": "#1a5f91", "name": "Stata" }, ".mathematica": { "color": "#dd1100", "name": "Mathematica" }, ".matlab": { "color": "#e16737", "name": "MATLAB" }, ".mawk": { "color": "#c30e9b", "name": "Awk" }, ".maxhelp": { "color": "#c4a79c", "name": "Max" }, ".maxpat": { "color": "#c4a79c", "name": "Max" }, ".maxproj": { "color": "#c4a79c", "name": "Max" }, ".mcfunction": { "color": "#E22837", "name": "mcfunction" }, ".mcr": { "color": "#00a6a6", "name": "MAXScript" }, ".mdoc": { "color": "#ecdebe", "name": "Roff Manpage" }, ".me": { "color": "#ecdebe", "name": "Roff" }, ".metal": { "color": "#8f14e9", "name": "Metal" }, ".mg": { "color": "#223388", "name": "Modula-3" }, ".mirah": { "color": "#c7a938", "name": "Mirah" }, ".mjs": { "color": "#f1e05a", "name": "JavaScript" }, ".mk": { "color": "#427819", "name": "Makefile" }, ".mkfile": { "color": "#427819", "name": "Makefile" }, ".mkii": { "color": "#3D6117", "name": "TeX" }, ".mkiv": { "color": "#3D6117", "name": "TeX" }, ".mkvi": { "color": "#3D6117", "name": "TeX" }, ".ml": { "color": "#dc566d", "name": "Standard ML" }, ".ml4": { "color": "#3be133", "name": "OCaml" }, ".mli": { "color": "#3be133", "name": "OCaml" }, ".mlir": { "color": "#5EC8DB", "name": "MLIR" }, ".mll": { "color": "#3be133", "name": "OCaml" }, ".mly": { "color": "#3be133", "name": "OCaml" }, ".mm": { "color": "#6866fb", "name": "Objective-C++" }, ".mo": { "color": "#de1d31", "name": "Modelica" }, ".mod": { "color": "#10253f", "name": "Modula-2" }, ".model.lkml": { "color": "#652B81", "name": "LookML" }, ".moo": { "color": "#ff2b2b", "name": "Mercury" }, ".moon": { "color": "#ff4585", "name": "MoonScript" }, ".mq4": { "color": "#62A8D6", "name": "MQL4" }, ".mq5": { "color": "#4A76B8", "name": "MQL5" }, ".mqh": { "color": "#4A76B8", "name": "MQL5" }, ".mrc": { "color": "#3d57c3", "name": "mIRC Script" }, ".ms": { "color": "#ecdebe", "name": "Roff" }, ".mspec": { "color": "#701516", "name": "Ruby" }, ".mt": { "color": "#dd1100", "name": "Mathematica" }, ".mtml": { "color": "#b7e1f4", "name": "MTML" }, ".mu": { "color": "#244963", "name": "mupad" }, ".mud": { "color": "#dc75e5", "name": "ZIL" }, ".mustache": { "color": "#724b3b", "name": "Mustache" }, ".mxt": { "color": "#c4a79c", "name": "Max" }, ".n": { "color": "#ecdebe", "name": "Roff" }, ".nasm": { "color": "#6E4C13", "name": "Assembly" }, ".nawk": { "color": "#c30e9b", "name": "Awk" }, ".nb": { "color": "#dd1100", "name": "Mathematica" }, ".nbp": { "color": "#dd1100", "name": "Mathematica" }, ".nc": { "color": "#94B0C7", "name": "nesC" }, ".ncl": { "color": "#28431f", "name": "NCL" }, ".ne": { "color": "#990000", "name": "Nearley" }, ".nearley": { "color": "#990000", "name": "Nearley" }, ".nf": { "color": "#3ac486", "name": "Nextflow" }, ".nim": { "color": "#ffc200", "name": "Nim" }, ".nim.cfg": { "color": "#ffc200", "name": "Nim" }, ".nimble": { "color": "#ffc200", "name": "Nim" }, ".nimrod": { "color": "#ffc200", "name": "Nim" }, ".nims": { "color": "#ffc200", "name": "Nim" }, ".nit": { "color": "#009917", "name": "Nit" }, ".nix": { "color": "#7e7eff", "name": "Nix" }, ".njk": { "color": "#3d8137", "name": "Nunjucks" }, ".njs": { "color": "#f1e05a", "name": "JavaScript" }, ".nl": { "color": "#87AED7", "name": "NewLisp" }, ".nlogo": { "color": "#ff6375", "name": "NetLogo" }, ".nqp": { "color": "#0000fb", "name": "Raku" }, ".nr": { "color": "#ecdebe", "name": "Roff" }, ".nse": { "color": "#000080", "name": "Lua" }, ".nss": { "color": "#111522", "name": "NWScript" }, ".nu": { "color": "#c9df40", "name": "Nu" }, ".numpy": { "color": "#9C8AF9", "name": "NumPy" }, ".numpyw": { "color": "#9C8AF9", "name": "NumPy" }, ".numsc": { "color": "#9C8AF9", "name": "NumPy" }, ".nut": { "color": "#800000", "name": "Squirrel" }, ".ny": { "color": "#3fb68b", "name": "Common Lisp" }, ".odin": { "color": "#60AFFE", "name": "Odin" }, ".ol": { "color": "#843179", "name": "Jolie" }, ".omgrofl": { "color": "#cabbff", "name": "Omgrofl" }, ".ooc": { "color": "#b0b77e", "name": "ooc" }, ".opal": { "color": "#f7ede0", "name": "Opal" }, ".opencl": { "color": "#ed2e2d", "name": "OpenCL" }, ".orc": { "color": "#1a1a1a", "name": "Csound" }, ".os": { "color": "#814CCC", "name": "1C Enterprise" }, ".oxygene": { "color": "#cdd0e3", "name": "Oxygene" }, ".oz": { "color": "#fab738", "name": "Oz" }, ".p": { "color": "#5ce600", "name": "OpenEdge ABL" }, ".p4": { "color": "#7055b5", "name": "P4" }, ".p6": { "color": "#0000fb", "name": "Raku" }, ".p6l": { "color": "#0000fb", "name": "Raku" }, ".p6m": { "color": "#0000fb", "name": "Raku" }, ".p8": { "color": "#000080", "name": "Lua" }, ".pac": { "color": "#f1e05a", "name": "JavaScript" }, ".pan": { "color": "#cc0000", "name": "Pan" }, ".parrot": { "color": "#f3ca0a", "name": "Parrot" }, ".pas": { "color": "#E3F171", "name": "Pascal" }, ".pascal": { "color": "#E3F171", "name": "Pascal" }, ".pat": { "color": "#c4a79c", "name": "Max" }, ".pb": { "color": "#5a6986", "name": "PureBasic" }, ".pbi": { "color": "#5a6986", "name": "PureBasic" }, ".pbt": { "color": "#8f0f8d", "name": "PowerBuilder" }, ".pck": { "color": "#dad8d8", "name": "PLSQL" }, ".pcss": { "color": "#dc3a0c", "name": "PostCSS" }, ".pd_lua": { "color": "#000080", "name": "Lua" }, ".pde": { "color": "#0096D8", "name": "Processing" }, ".pegjs": { "color": "#234d6b", "name": "PEG.js" }, ".pep": { "color": "#C76F5B", "name": "Pep8" }, ".perl": { "color": "#0298c3", "name": "Perl" }, ".pfa": { "color": "#da291c", "name": "PostScript" }, ".pgsql": { "color": "#336790", "name": "PLpgSQL" }, ".ph": { "color": "#0298c3", "name": "Perl" }, ".php": { "color": "#4F5D95", "name": "PHP" }, ".php3": { "color": "#4F5D95", "name": "PHP" }, ".php4": { "color": "#4F5D95", "name": "PHP" }, ".php5": { "color": "#4F5D95", "name": "PHP" }, ".phps": { "color": "#4F5D95", "name": "PHP" }, ".phpt": { "color": "#4F5D95", "name": "PHP" }, ".phtml": { "color": "#4f5d95", "name": "HTML+PHP" }, ".pig": { "color": "#fcd7de", "name": "PigLatin" }, ".pike": { "color": "#005390", "name": "Pike" }, ".pkb": { "color": "#dad8d8", "name": "PLSQL" }, ".pks": { "color": "#dad8d8", "name": "PLSQL" }, ".pl": { "color": "#0000fb", "name": "Raku" }, ".pl6": { "color": "#0000fb", "name": "Raku" }, ".plb": { "color": "#dad8d8", "name": "PLSQL" }, ".plot": { "color": "#f0a9f0", "name": "Gnuplot" }, ".pls": { "color": "#dad8d8", "name": "PLSQL" }, ".plsql": { "color": "#dad8d8", "name": "PLSQL" }, ".plt": { "color": "#f0a9f0", "name": "Gnuplot" }, ".pluginspec": { "color": "#701516", "name": "Ruby" }, ".plx": { "color": "#0298c3", "name": "Perl" }, ".pm": { "color": "#0000fb", "name": "Raku" }, ".pm6": { "color": "#0000fb", "name": "Raku" }, ".pmod": { "color": "#005390", "name": "Pike" }, ".podsl": { "color": "#3fb68b", "name": "Common Lisp" }, ".podspec": { "color": "#701516", "name": "Ruby" }, ".pogo": { "color": "#d80074", "name": "PogoScript" }, ".postcss": { "color": "#dc3a0c", "name": "PostCSS" }, ".pov": { "color": "#6bac65", "name": "POV-Ray SDL" }, ".pp": { "color": "#302B6D", "name": "Puppet" }, ".pprx": { "color": "#d90e09", "name": "REXX" }, ".prawn": { "color": "#701516", "name": "Ruby" }, ".prc": { "color": "#dad8d8", "name": "PLSQL" }, ".prg": { "color": "#403a40", "name": "xBase" }, ".pro": { "color": "#74283c", "name": "Prolog" }, ".prolog": { "color": "#74283c", "name": "Prolog" }, ".prw": { "color": "#403a40", "name": "xBase" }, ".ps": { "color": "#da291c", "name": "PostScript" }, ".ps1": { "color": "#012456", "name": "PowerShell" }, ".psc": { "color": "#6600cc", "name": "Papyrus" }, ".psd1": { "color": "#012456", "name": "PowerShell" }, ".psgi": { "color": "#0298c3", "name": "Perl" }, ".psm1": { "color": "#012456", "name": "PowerShell" }, ".pug": { "color": "#a86454", "name": "Pug" }, ".purs": { "color": "#1D222D", "name": "PureScript" }, ".pwn": { "color": "#dbb284", "name": "Pawn" }, ".pxd": { "color": "#fedf5b", "name": "Cython" }, ".pxi": { "color": "#fedf5b", "name": "Cython" }, ".py": { "color": "#3572A5", "name": "Python" }, ".py3": { "color": "#3572A5", "name": "Python" }, ".pyde": { "color": "#3572A5", "name": "Python" }, ".pyi": { "color": "#3572A5", "name": "Python" }, ".pyp": { "color": "#3572A5", "name": "Python" }, ".pyt": { "color": "#3572A5", "name": "Python" }, ".pyw": { "color": "#3572A5", "name": "Python" }, ".pyx": { "color": "#fedf5b", "name": "Cython" }, ".q": { "color": "#0040cd", "name": "q" }, ".qasm": { "color": "#AA70FF", "name": "OpenQASM" }, ".qbs": { "color": "#44a51c", "name": "QML" }, ".ql": { "color": "#140f46", "name": "CodeQL" }, ".qll": { "color": "#140f46", "name": "CodeQL" }, ".qml": { "color": "#44a51c", "name": "QML" }, ".qs": { "color": "#00b841", "name": "Qt Script" }, ".r": { "color": "#358a5b", "name": "Rebol" }, ".r2": { "color": "#358a5b", "name": "Rebol" }, ".r3": { "color": "#358a5b", "name": "Rebol" }, ".rabl": { "color": "#701516", "name": "Ruby" }, ".rake": { "color": "#701516", "name": "Ruby" }, ".raku": { "color": "#0000fb", "name": "Raku" }, ".rakumod": { "color": "#0000fb", "name": "Raku" }, ".raml": { "color": "#77d9fb", "name": "RAML" }, ".razor": { "color": "#512be4", "name": "HTML+Razor" }, ".rb": { "color": "#701516", "name": "Ruby" }, ".rbi": { "color": "#701516", "name": "Ruby" }, ".rbuild": { "color": "#701516", "name": "Ruby" }, ".rbw": { "color": "#701516", "name": "Ruby" }, ".rbx": { "color": "#701516", "name": "Ruby" }, ".rbxs": { "color": "#000080", "name": "Lua" }, ".rd": { "color": "#198CE7", "name": "R" }, ".re": { "color": "#ff5847", "name": "Reason" }, ".reb": { "color": "#358a5b", "name": "Rebol" }, ".rebol": { "color": "#358a5b", "name": "Rebol" }, ".red": { "color": "#f50000", "name": "Red" }, ".reds": { "color": "#f50000", "name": "Red" }, ".rego": { "color": "#7d9199", "name": "Open Policy Agent" }, ".rei": { "color": "#ff5847", "name": "Reason" }, ".res": { "color": "#ed5051", "name": "ReScript" }, ".rex": { "color": "#d90e09", "name": "REXX" }, ".rexx": { "color": "#d90e09", "name": "REXX" }, ".rg": { "color": "#cc0088", "name": "Rouge" }, ".rhtml": { "color": "#701516", "name": "HTML+ERB" }, ".ring": { "color": "#2D54CB", "name": "Ring" }, ".riot": { "color": "#A71E49", "name": "Riot" }, ".rkt": { "color": "#3c5caa", "name": "Racket" }, ".rktd": { "color": "#3c5caa", "name": "Racket" }, ".rktl": { "color": "#3c5caa", "name": "Racket" }, ".rl": { "color": "#9d5200", "name": "Ragel" }, ".rnh": { "color": "#665a4e", "name": "RUNOFF" }, ".rno": { "color": "#ecdebe", "name": "Roff" }, ".robot": { "color": "#00c0b5", "name": "RobotFramework" }, ".rockspec": { "color": "#000080", "name": "Lua" }, ".roff": { "color": "#ecdebe", "name": "Roff" }, ".rpy": { "color": "#ff7f7f", "name": "Ren'Py" }, ".rs": { "color": "#dea584", "name": "Rust" }, ".rs.in": { "color": "#dea584", "name": "Rust" }, ".rsc": { "color": "#fffaa0", "name": "Rascal" }, ".rsx": { "color": "#198CE7", "name": "R" }, ".ru": { "color": "#701516", "name": "Ruby" }, ".ruby": { "color": "#701516", "name": "Ruby" }, ".s": { "color": "#005daa", "name": "Motorola 68K Assembly" }, ".sas": { "color": "#B34936", "name": "SAS" }, ".sass": { "color": "#a53b70", "name": "Sass" }, ".sats": { "color": "#1ac620", "name": "ATS" }, ".sbt": { "color": "#c22d40", "name": "Scala" }, ".sc": { "color": "#46390b", "name": "SuperCollider" }, ".scad": { "color": "#e5cd45", "name": "OpenSCAD" }, ".scala": { "color": "#c22d40", "name": "Scala" }, ".scaml": { "color": "#bd181a", "name": "Scaml" }, ".scd": { "color": "#46390b", "name": "SuperCollider" }, ".sce": { "color": "#ca0f21", "name": "Scilab" }, ".sch": { "color": "#1e4aec", "name": "Scheme" }, ".sci": { "color": "#ca0f21", "name": "Scilab" }, ".scm": { "color": "#1e4aec", "name": "Scheme" }, ".sco": { "color": "#1a1a1a", "name": "Csound Score" }, ".scpt": { "color": "#101F1F", "name": "AppleScript" }, ".scrbl": { "color": "#3c5caa", "name": "Racket" }, ".scss": { "color": "#c6538c", "name": "SCSS" }, ".sed": { "color": "#64b970", "name": "sed" }, ".self": { "color": "#0579aa", "name": "Self" }, ".sexp": { "color": "#3fb68b", "name": "Common Lisp" }, ".sh": { "color": "#89e051", "name": "Shell" }, ".sh.in": { "color": "#89e051", "name": "Shell" }, ".shader": { "color": "#222c37", "name": "ShaderLab" }, ".shen": { "color": "#120F14", "name": "Shen" }, ".sig": { "color": "#dc566d", "name": "Standard ML" }, ".sj": { "color": "#ff0c5a", "name": "Objective-J" }, ".sjs": { "color": "#f1e05a", "name": "JavaScript" }, ".sl": { "color": "#007eff", "name": "Slash" }, ".sld": { "color": "#1e4aec", "name": "Scheme" }, ".slim": { "color": "#2b2b2b", "name": "Slim" }, ".sls": { "color": "#1e4aec", "name": "Scheme" }, ".sma": { "color": "#dbb284", "name": "Pawn" }, ".smk": { "color": "#3572A5", "name": "Python" }, ".sml": { "color": "#dc566d", "name": "Standard ML" }, ".snip": { "color": "#199f4b", "name": "Vim Snippet" }, ".snippet": { "color": "#199f4b", "name": "Vim Snippet" }, ".snippets": { "color": "#199f4b", "name": "Vim Snippet" }, ".sol": { "color": "#AA6746", "name": "Solidity" }, ".soy": { "color": "#0d948f", "name": "Closure Templates" }, ".sp": { "color": "#f69e1d", "name": "SourcePawn" }, ".spc": { "color": "#dad8d8", "name": "PLSQL" }, ".spec": { "color": "#701516", "name": "Ruby" }, ".spin": { "color": "#7fa2a7", "name": "Propeller Spin" }, ".sps": { "color": "#1e4aec", "name": "Scheme" }, ".sqf": { "color": "#3F3F3F", "name": "SQF" }, ".sql": { "color": "#e38c00", "name": "TSQL" }, ".sra": { "color": "#8f0f8d", "name": "PowerBuilder" }, ".srt": { "color": "#348a34", "name": "SRecode Template" }, ".sru": { "color": "#8f0f8d", "name": "PowerBuilder" }, ".srw": { "color": "#8f0f8d", "name": "PowerBuilder" }, ".ss": { "color": "#1e4aec", "name": "Scheme" }, ".ssjs": { "color": "#f1e05a", "name": "JavaScript" }, ".sss": { "color": "#2fcc9f", "name": "SugarSS" }, ".st": { "color": "#3fb34f", "name": "StringTemplate" }, ".stan": { "color": "#b2011d", "name": "Stan" }, ".sthlp": { "color": "#1a5f91", "name": "Stata" }, ".story": { "color": "#5B2063", "name": "Gherkin" }, ".sty": { "color": "#3D6117", "name": "TeX" }, ".styl": { "color": "#ff6347", "name": "Stylus" }, ".sv": { "color": "#DAE1C2", "name": "SystemVerilog" }, ".svelte": { "color": "#ff3e00", "name": "Svelte" }, ".svh": { "color": "#DAE1C2", "name": "SystemVerilog" }, ".swift": { "color": "#F05138", "name": "Swift" }, ".t": { "color": "#cf142b", "name": "Turing" }, ".tac": { "color": "#3572A5", "name": "Python" }, ".tcc": { "color": "#f34b7d", "name": "C++" }, ".tcl": { "color": "#e4cc98", "name": "Tcl" }, ".tcl.in": { "color": "#e4cc98", "name": "Tcl" }, ".tesc": { "color": "#5686a5", "name": "GLSL" }, ".tese": { "color": "#5686a5", "name": "GLSL" }, ".tex": { "color": "#3D6117", "name": "TeX" }, ".thor": { "color": "#701516", "name": "Ruby" }, ".thrift": { "color": "#D12127", "name": "Thrift" }, ".thy": { "color": "#FEFE00", "name": "Isabelle" }, ".tla": { "color": "#4b0079", "name": "TLA" }, ".tm": { "color": "#e4cc98", "name": "Tcl" }, ".tmac": { "color": "#ecdebe", "name": "Roff" }, ".tmux": { "color": "#89e051", "name": "Shell" }, ".toc": { "color": "#3D6117", "name": "TeX" }, ".tool": { "color": "#89e051", "name": "Shell" }, ".tpb": { "color": "#dad8d8", "name": "PLSQL" }, ".tpl": { "color": "#f0c040", "name": "Smarty" }, ".tpp": { "color": "#f34b7d", "name": "C++" }, ".tps": { "color": "#dad8d8", "name": "PLSQL" }, ".trg": { "color": "#dad8d8", "name": "PLSQL" }, ".ts": { "color": "#2b7489", "name": "TypeScript" }, ".tst": { "color": "#ca0f21", "name": "Scilab" }, ".tsx": { "color": "#2b7489", "name": "TypeScript" }, ".tu": { "color": "#cf142b", "name": "Turing" }, ".twig": { "color": "#c1d026", "name": "Twig" }, ".txl": { "color": "#0178b8", "name": "TXL" }, ".uc": { "color": "#a54c4d", "name": "UnrealScript" }, ".udo": { "color": "#1a1a1a", "name": "Csound" }, ".uno": { "color": "#9933cc", "name": "Uno" }, ".upc": { "color": "#4e3617", "name": "Unified Parallel C" }, ".ur": { "color": "#ccccee", "name": "UrWeb" }, ".urs": { "color": "#ccccee", "name": "UrWeb" }, ".v": { "color": "#b2b7f8", "name": "Verilog" }, ".vala": { "color": "#fbe5cd", "name": "Vala" }, ".vapi": { "color": "#fbe5cd", "name": "Vala" }, ".vark": { "color": "#82937f", "name": "Gosu" }, ".vb": { "color": "#945db7", "name": "Visual Basic .NET" }, ".vba": { "color": "#199f4b", "name": "Vim Script" }, ".vbhtml": { "color": "#945db7", "name": "Visual Basic .NET" }, ".vbs": { "color": "#15dcdc", "name": "VBScript" }, ".vcl": { "color": "#148AA8", "name": "VCL" }, ".veo": { "color": "#b2b7f8", "name": "Verilog" }, ".vert": { "color": "#5686a5", "name": "GLSL" }, ".vh": { "color": "#DAE1C2", "name": "SystemVerilog" }, ".vhd": { "color": "#adb2cb", "name": "VHDL" }, ".vhdl": { "color": "#adb2cb", "name": "VHDL" }, ".vhf": { "color": "#adb2cb", "name": "VHDL" }, ".vhi": { "color": "#adb2cb", "name": "VHDL" }, ".vho": { "color": "#adb2cb", "name": "VHDL" }, ".vhs": { "color": "#adb2cb", "name": "VHDL" }, ".vht": { "color": "#adb2cb", "name": "VHDL" }, ".vhw": { "color": "#adb2cb", "name": "VHDL" }, ".view.lkml": { "color": "#652B81", "name": "LookML" }, ".vim": { "color": "#199f4b", "name": "Vim Script" }, ".vmb": { "color": "#199f4b", "name": "Vim Script" }, ".volt": { "color": "#1F1F1F", "name": "Volt" }, ".vrx": { "color": "#5686a5", "name": "GLSL" }, ".vsh": { "color": "#5686a5", "name": "GLSL" }, ".vshader": { "color": "#5686a5", "name": "GLSL" }, ".vue": { "color": "#41b883", "name": "Vue" }, ".vw": { "color": "#dad8d8", "name": "PLSQL" }, ".w": { "color": "#5ce600", "name": "OpenEdge ABL" }, ".wast": { "color": "#04133b", "name": "WebAssembly" }, ".wat": { "color": "#04133b", "name": "WebAssembly" }, ".watchr": { "color": "#701516", "name": "Ruby" }, ".wdl": { "color": "#42f1f4", "name": "wdl" }, ".wisp": { "color": "#7582D1", "name": "wisp" }, ".wl": { "color": "#dd1100", "name": "Mathematica" }, ".wlk": { "color": "#a23738", "name": "Wollok" }, ".wlt": { "color": "#dd1100", "name": "Mathematica" }, ".wlua": { "color": "#000080", "name": "Lua" }, ".wsgi": { "color": "#3572A5", "name": "Python" }, ".x10": { "color": "#4B6BEF", "name": "X10" }, ".x68": { "color": "#005daa", "name": "Motorola 68K Assembly" }, ".xc": { "color": "#99DA07", "name": "XC" }, ".xht": { "color": "#e34c26", "name": "HTML" }, ".xhtml": { "color": "#e34c26", "name": "HTML" }, ".xojo_code": { "color": "#81bd41", "name": "Xojo" }, ".xojo_menu": { "color": "#81bd41", "name": "Xojo" }, ".xojo_report": { "color": "#81bd41", "name": "Xojo" }, ".xojo_script": { "color": "#81bd41", "name": "Xojo" }, ".xojo_toolbar": { "color": "#81bd41", "name": "Xojo" }, ".xojo_window": { "color": "#81bd41", "name": "Xojo" }, ".xpy": { "color": "#3572A5", "name": "Python" }, ".xq": { "color": "#5232e7", "name": "XQuery" }, ".xql": { "color": "#5232e7", "name": "XQuery" }, ".xqm": { "color": "#5232e7", "name": "XQuery" }, ".xquery": { "color": "#5232e7", "name": "XQuery" }, ".xqy": { "color": "#5232e7", "name": "XQuery" }, ".xrl": { "color": "#B83998", "name": "Erlang" }, ".xsh": { "color": "#285EEF", "name": "Xonsh" }, ".xsjs": { "color": "#f1e05a", "name": "JavaScript" }, ".xsjslib": { "color": "#f1e05a", "name": "JavaScript" }, ".xsl": { "color": "#EB8CEB", "name": "XSLT" }, ".xslt": { "color": "#EB8CEB", "name": "XSLT" }, ".xtend": { "color": "#24255d", "name": "Xtend" }, ".xzap": { "color": "#0d665e", "name": "ZAP" }, ".y": { "color": "#4B6C4B", "name": "Yacc" }, ".yacc": { "color": "#4B6C4B", "name": "Yacc" }, ".yap": { "color": "#74283c", "name": "Prolog" }, ".yar": { "color": "#220000", "name": "YARA" }, ".yara": { "color": "#220000", "name": "YARA" }, ".yasnippet": { "color": "#32AB90", "name": "YASnippet" }, ".yrl": { "color": "#B83998", "name": "Erlang" }, ".yy": { "color": "#4B6C4B", "name": "Yacc" }, ".zap": { "color": "#0d665e", "name": "ZAP" }, ".zep": { "color": "#118f9e", "name": "Zephir" }, ".zig": { "color": "#ec915c", "name": "Zig" }, ".zil": { "color": "#dc75e5", "name": "ZIL" }, ".zimpl": { "color": "#d67711", "name": "Zimpl" }, ".zmpl": { "color": "#d67711", "name": "Zimpl" }, ".zpl": { "color": "#d67711", "name": "Zimpl" }, ".zs": { "color": "#00BCD1", "name": "ZenScript" }, ".zsh": { "color": "#89e051", "name": "Shell" } } ================================================ FILE: backend/src/data/github/graphql/__init__.py ================================================ from src.data.github.graphql.commit import get_commits from src.data.github.graphql.models import RawCommit, RawRepo from src.data.github.graphql.repo import get_repo from src.data.github.graphql.template import ( GraphQLErrorMissingNode, GraphQLErrorRateLimit, GraphQLErrorTimeout, get_query_limit, ) from src.data.github.graphql.user.contribs.contribs import ( get_user_contribution_calendar, get_user_contribution_events, ) from src.data.github.graphql.user.contribs.models import ( RawCalendar, RawEvents, RawEventsCommit, RawEventsEvent, ) from src.data.github.graphql.user.follows.follows import ( get_user_followers, get_user_following, ) from src.data.github.graphql.user.follows.models import RawFollows __all__ = [ "get_commits", "RawCommit", "RawRepo", "get_repo", "GraphQLErrorMissingNode", "GraphQLErrorRateLimit", "GraphQLErrorTimeout", "get_query_limit", "get_user_contribution_calendar", "get_user_contribution_events", "RawCalendar", "RawEvents", "RawEventsCommit", "RawEventsEvent", "get_user_followers", "get_user_following", "RawFollows", ] ================================================ FILE: backend/src/data/github/graphql/commit.py ================================================ from typing import List, Optional from src.constants import PR_FILES from src.data.github.graphql.models import RawCommit from src.data.github.graphql.template import ( GraphQLError, GraphQLErrorMissingNode, GraphQLErrorRateLimit, GraphQLErrorTimeout, get_template, ) def get_commits( node_ids: List[str], access_token: Optional[str] = None, catch_errors: bool = False ) -> List[Optional[RawCommit]]: """ Gets all repository data from graphql :param access_token: GitHub access token :param node_ids: List of node ids :return: List of commits """ if PR_FILES == 0: # type: ignore query = { "variables": {"ids": node_ids}, "query": """ query getCommits($ids: [ID!]!) { nodes(ids: $ids) { ... on Commit { additions deletions changedFiles url } } } """, } else: query = { "variables": {"ids": node_ids, "first": PR_FILES}, "query": """ query getCommits($ids: [ID!]!, $first: Int!) { nodes(ids: $ids) { ... on Commit { additions deletions changedFiles url associatedPullRequests(first: 1) { nodes { changedFiles additions deletions files(first: $first) { nodes { path additions deletions } } } } } } } """, } try: raw_commits = get_template(query, access_token)["data"]["nodes"] except GraphQLErrorMissingNode as e: return ( get_commits(node_ids[: e.node], access_token) + [None] + get_commits(node_ids[e.node + 1 :], access_token) ) except (GraphQLErrorRateLimit, GraphQLErrorTimeout, GraphQLError) as e: if catch_errors: return [None for _ in node_ids] raise e out: List[Optional[RawCommit]] = [] for raw_commit in raw_commits: try: if "associatedPullRequests" not in raw_commit: raw_commit["associatedPullRequests"] = {"nodes": []} out.append(RawCommit.model_validate(raw_commit)) except Exception as e: if catch_errors: out.append(None) else: raise e return out ================================================ FILE: backend/src/data/github/graphql/models.py ================================================ from typing import List, Optional from pydantic import BaseModel, Field class RawCommitPRFileNode(BaseModel): path: str additions: int deletions: int class RawCommitPRFile(BaseModel): nodes: List[RawCommitPRFileNode] class RawCommitPRNode(BaseModel): changed_files: int = Field(alias="changedFiles") additions: int deletions: int files: RawCommitPRFile class RawCommitPR(BaseModel): nodes: List[RawCommitPRNode] class RawCommit(BaseModel): additions: int deletions: int changed_files: int = Field(alias="changedFiles") url: str prs: RawCommitPR = Field(alias="associatedPullRequests") class RawRepoLanguageNode(BaseModel): name: str color: Optional[str] class RawRepoLanguageEdge(BaseModel): node: RawRepoLanguageNode size: int class RawRepoLanguage(BaseModel): total_count: int = Field(alias="totalCount") total_size: int = Field(alias="totalSize") edges: List[RawRepoLanguageEdge] class RawRepo(BaseModel): is_private: bool = Field(alias="isPrivate") fork_count: int = Field(alias="forkCount") stargazer_count: int = Field(alias="stargazerCount") languages: RawRepoLanguage ================================================ FILE: backend/src/data/github/graphql/repo.py ================================================ from typing import Optional from src.data.github.graphql.models import RawRepo from src.data.github.graphql.template import get_template def get_repo( owner: str, repo: str, access_token: Optional[str] = None, catch_errors: bool = False, ) -> Optional[RawRepo]: """ Gets all repository data from graphql :param access_token: GitHub access token :param owner: GitHub owner :param repo: GitHub repository :return: RawRepo object or None if repo not present """ query = { "variables": {"owner": owner, "repo": repo}, "query": """ query getRepo($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { isPrivate, forkCount, stargazerCount, languages(first: 10){ totalCount, totalSize, edges{ node { name, color, }, size, }, }, } } """, } try: raw_repo = get_template(query, access_token)["data"]["repository"] return RawRepo.model_validate(raw_repo) except Exception as e: if catch_errors: return None raise e ================================================ FILE: backend/src/data/github/graphql/template.py ================================================ import logging from datetime import datetime from typing import Any, Dict, Optional, Tuple import requests from requests.exceptions import ReadTimeout from src.constants import TIMEOUT from src.data.github.utils import get_access_token s = requests.session() class GraphQLError(Exception): pass class GraphQLErrorMissingNode(Exception): def __init__(self, node: int, *args: Tuple[Any], **kwargs: Dict[str, Any]): super(Exception, self).__init__(*args, **kwargs) self.node = node class GraphQLErrorRateLimit(Exception): pass class GraphQLErrorTimeout(Exception): pass def get_template( query: Dict[str, Any], access_token: Optional[str] = None, retries: int = 0 ) -> Dict[str, Any]: """ Template for interacting with the GitHub GraphQL API :param query: The query to be sent to the GitHub GraphQL API :param access_token: The access token to be used for the query :param retries: The number of retries to be made for Auth Exceptions :return: The response from the GitHub GraphQL API """ start = datetime.now() new_access_token = get_access_token(access_token) headers: Dict[str, str] = {"Authorization": f"bearer {new_access_token}"} try: r = s.post( "https://api.github.com/graphql", json=query, headers=headers, timeout=TIMEOUT, ) except ReadTimeout: raise GraphQLErrorTimeout("GraphQL Error: Request Timeout") print("GraphQL", new_access_token, datetime.now() - start) if r.status_code == 200: data = r.json() if "errors" in data: if ( "type" in data["errors"][0] and data["errors"][0]["type"] in ["SERVICE_UNAVAILABLE", "NOT_FOUND"] and "path" in data["errors"][0] and isinstance(data["errors"][0]["path"], list) and data["errors"][0]["path"][0] == "nodes" ): raise GraphQLErrorMissingNode(node=int(data["errors"][0]["path"][1])) if retries < 2: print("GraphQL Error, Retrying:", new_access_token) return get_template(query, access_token, retries + 1) raise GraphQLError("GraphQL Error: " + str(data["errors"])) return data if r.status_code in [401, 403]: if retries < 2: print("GraphQL Error, Retrying:", new_access_token) return get_template(query, access_token, retries + 1) raise GraphQLErrorRateLimit("GraphQL Error: Unauthorized") if r.status_code == 502: raise GraphQLErrorTimeout("GraphQL Error: Request Timeout") raise GraphQLError(f"GraphQL Error: {str(r.status_code)}") def get_query_limit(access_token: str) -> int: """ Get the current rate limit for the GitHub GraphQL API :param access_token: The access token to be used for the query :return: The current rate limit for the GitHub GraphQL API """ try: data = get_template( {"query": "query { rateLimit { remaining } }"}, access_token ) return data["data"]["rateLimit"]["remaining"] except Exception as e: logging.exception(e) return -1 ================================================ FILE: backend/src/data/github/graphql/user/__init__.py ================================================ ================================================ FILE: backend/src/data/github/graphql/user/contribs/__init__.py ================================================ ================================================ FILE: backend/src/data/github/graphql/user/contribs/contribs.py ================================================ # import json from datetime import datetime from typing import Optional from src.data.github.graphql.template import get_template from src.data.github.graphql.user.contribs.models import RawCalendar, RawEvents def get_user_contribution_calendar( user_id: str, start_date: datetime, end_date: datetime, access_token: Optional[str] = None, ) -> RawCalendar: """Gets contribution calendar for a given time period (max one year)""" if (end_date - start_date).days > 365: raise ValueError("date range can be at most 1 year") query = { "variables": { "login": user_id, "startDate": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "endDate": end_date.strftime("%Y-%m-%dT%H:%M:%SZ"), }, "query": """ query getUser($login: String!, $startDate: DateTime!, $endDate: DateTime!){ user(login: $login){ contributionsCollection(from: $startDate, to: $endDate){ contributionCalendar{ weeks{ contributionDays{ date weekday contributionCount } } } } } } """, } raw_data = get_template(query, access_token) output = raw_data["data"]["user"]["contributionsCollection"]["contributionCalendar"] return RawCalendar.model_validate(output) def get_user_contribution_events( user_id: str, start_date: datetime, end_date: datetime, max_repos: int = 100, first: int = 100, after: str = "", access_token: Optional[str] = None, ) -> RawEvents: """Fetches user contributions (commits, issues, prs, reviews)""" query = { "variables": { "login": user_id, "startDate": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "endDate": end_date.strftime("%Y-%m-%dT%H:%M:%SZ"), "maxRepos": max_repos, "first": first, "after": after, }, "query": """ query getUser($login: String!, $startDate: DateTime!, $endDate: DateTime!, $maxRepos: Int!, $first: Int!, $after: String!) { user(login: $login){ contributionsCollection(from: $startDate, to: $endDate){ commitContributionsByRepository(maxRepositories: $maxRepos){ repository{ nameWithOwner, }, totalCount:contributions(first: 1){ totalCount } contributions(first: $first, after: $after){ nodes{ commitCount, occurredAt, } pageInfo{ hasNextPage, endCursor } } } issueContributionsByRepository(maxRepositories: $maxRepos){ repository{ nameWithOwner }, totalCount:contributions(first: 1){ totalCount } contributions(first: $first, after: $after){ nodes{ occurredAt, } pageInfo{ hasNextPage, endCursor } } } pullRequestContributionsByRepository(maxRepositories: $maxRepos){ repository{ nameWithOwner }, totalCount:contributions(first: 1){ totalCount } contributions(first: $first, after: $after){ nodes{ occurredAt, } pageInfo{ hasNextPage, endCursor } } } pullRequestReviewContributionsByRepository(maxRepositories: $maxRepos){ repository{ nameWithOwner }, totalCount:contributions(first: 1){ totalCount } contributions(first: $first, after: $after){ nodes{ occurredAt, } pageInfo{ hasNextPage, endCursor } } }, repositoryContributions(first: $maxRepos){ totalCount nodes{ repository{ nameWithOwner, } occurredAt, } }, }, } } """, } raw_data = get_template(query, access_token) output = raw_data["data"]["user"]["contributionsCollection"] return RawEvents.model_validate(output) ================================================ FILE: backend/src/data/github/graphql/user/contribs/models.py ================================================ from datetime import date, datetime from typing import List, Optional from pydantic import BaseModel, Field class RawCalendarDay(BaseModel): date: date weekday: int count: int = Field(alias="contributionCount") class RawCalendarWeek(BaseModel): contribution_days: List[RawCalendarDay] = Field(alias="contributionDays") class RawCalendar(BaseModel): weeks: List[RawCalendarWeek] class RawEventsRepoName(BaseModel): name: str = Field(alias="nameWithOwner") class RawEventsCount(BaseModel): count: int = Field(alias="totalCount") class RawEventsCommit(BaseModel): count: int = Field(alias="commitCount") occurred_at: datetime = Field(alias="occurredAt") class RawEventsEvent(BaseModel): occurred_at: datetime = Field(alias="occurredAt") class RawEventsPageInfo(BaseModel): has_next_page: bool = Field(alias="hasNextPage") end_cursor: Optional[str] = Field(alias="endCursor") class Config: allow_none = True class RawEventsCommits(BaseModel): nodes: List[RawEventsCommit] page_info: RawEventsPageInfo = Field(alias="pageInfo") class RawEventsContribs(BaseModel): nodes: List[RawEventsEvent] page_info: RawEventsPageInfo = Field(alias="pageInfo") class RawEventsRepoCommits(BaseModel): repo: RawEventsRepoName = Field(alias="repository") count: RawEventsCount = Field(alias="totalCount") contribs: RawEventsCommits = Field(alias="contributions") class RawEventsRepo(BaseModel): repo: RawEventsRepoName = Field(alias="repository") count: RawEventsCount = Field(alias="totalCount") contribs: RawEventsContribs = Field(alias="contributions") class RawEventsRepoEvent(BaseModel): repo: RawEventsRepoName = Field(alias="repository") occurred_at: datetime = Field(alias="occurredAt") class RawEventsRepoContribs(BaseModel): count: int = Field(alias="totalCount") nodes: List[RawEventsRepoEvent] class RawEvents(BaseModel): commit_contribs_by_repo: List[RawEventsRepoCommits] = Field( alias="commitContributionsByRepository" ) issue_contribs_by_repo: List[RawEventsRepo] = Field( alias="issueContributionsByRepository" ) pr_contribs_by_repo: List[RawEventsRepo] = Field( alias="pullRequestContributionsByRepository" ) review_contribs_by_repo: List[RawEventsRepo] = Field( alias="pullRequestReviewContributionsByRepository" ) repo_contribs: RawEventsRepoContribs = Field(alias="repositoryContributions") ================================================ FILE: backend/src/data/github/graphql/user/follows/__init__.py ================================================ ================================================ FILE: backend/src/data/github/graphql/user/follows/follows.py ================================================ # import json from typing import Dict, Optional, Union from src.data.github.graphql.template import get_template from src.data.github.graphql.user.follows.models import RawFollows def get_user_followers( user_id: str, first: int = 100, after: str = "", access_token: Optional[str] = None ) -> RawFollows: """gets user's followers and users following'""" variables: Dict[str, Union[str, int]] = ( {"login": user_id, "first": first, "after": after} if after != "" else {"login": user_id, "first": first} ) query_str: str = ( """ query getUser($login: String!, $first: Int!, $after: String!) { user(login: $login){ followers(first: $first, after: $after){ nodes{ name, login, url } pageInfo{ hasNextPage, endCursor } } } } """ if after != "" else """ query getUser($login: String!, $first: Int!) { user(login: $login){ followers(first: $first){ nodes{ name, login, url } pageInfo{ hasNextPage, endCursor } } } } """ ) query = { "variables": variables, "query": query_str, } output_dict = get_template(query, access_token)["data"]["user"]["followers"] return RawFollows.model_validate(output_dict) def get_user_following( user_id: str, first: int = 10, after: str = "", access_token: Optional[str] = None ) -> RawFollows: """gets user's followers and users following'""" variables: Dict[str, Union[str, int]] = ( {"login": user_id, "first": first, "after": after} if after != "" else {"login": user_id, "first": first} ) query_str: str = ( """ query getUser($login: String!, $first: Int!, $after: String!) { user(login: $login){ following(first: $first, after: $after){ nodes{ name, login, url } pageInfo{ hasNextPage, endCursor } } } } """ if after != "" else """ query getUser($login: String!, $first: Int!) { user(login: $login){ following(first: $first){ nodes{ name, login, url } pageInfo{ hasNextPage, endCursor } } } } """ ) query = { "variables": variables, "query": query_str, } output_dict = get_template(query, access_token)["data"]["user"]["following"] return RawFollows.model_validate(output_dict) ================================================ FILE: backend/src/data/github/graphql/user/follows/models.py ================================================ from typing import List, Optional from pydantic import BaseModel, Field from src.models import User class PageInfo(BaseModel): has_next_page: bool = Field(alias="hasNextPage") end_cursor: Optional[str] = Field(alias="endCursor") class Config: allow_none = True class RawFollows(BaseModel): nodes: List[User] page_info: PageInfo = Field(alias="pageInfo") ================================================ FILE: backend/src/data/github/language_map.py ================================================ import json import urllib.request from typing import Any, Dict BLACKLIST = [".md"] with urllib.request.urlopen( "https://raw.githubusercontent.com/blakeembrey/language-map/main/languages.json" ) as url: data: Dict[str, Dict[str, Any]] = json.loads(url.read().decode()) languages = { k: v for k, v in data.items() if v["type"] in ["programming", "markup"] and "color" in v and "extensions" in v } extensions: Dict[str, Dict[str, str]] = {} for lang_name, lang in languages.items(): for extension in lang["extensions"]: if extension not in BLACKLIST: extensions[extension] = {"color": lang["color"], "name": lang_name} extensions = dict(sorted(extensions.items(), key=lambda x: x[0])) extensions[".tsx"]["name"] = "TypeScript" extensions[".tsx"]["color"] = "#2B7489" extensions[".cs"]["name"] = "C#" extensions[".cs"]["color"] = "#178600" extensions[".ml"]["name"] = "OCaml" extensions[".ml"]["color"] = "#3BE133" with open("src/data/github/extensions.json", "w") as f: json.dump(extensions, f, indent=4) ================================================ FILE: backend/src/data/github/rest/__init__.py ================================================ from src.data.github.rest.commit import get_commit_files from src.data.github.rest.models import RawCommit, RawCommitFile from src.data.github.rest.repo import get_repo_commits, get_repo_stargazers from src.data.github.rest.template import RESTError, RESTErrorNotFound from src.data.github.rest.user import get_user, get_user_starred_repos __all__ = [ "get_commit_files", "RawCommit", "RawCommitFile", "get_repo_commits", "get_repo_stargazers", "RESTError", "RESTErrorNotFound", "get_user", "get_user_starred_repos", ] ================================================ FILE: backend/src/data/github/rest/commit.py ================================================ from typing import List, Optional from src.data.github.rest.models import RawCommitFile from src.data.github.rest.template import get_template BASE_URL = "https://api.github.com/repos/" def get_commit_files( owner: str, repo: str, sha: str, access_token: Optional[str] = None ) -> Optional[List[RawCommitFile]]: """ Returns raw repository data :param owner: repository owner :param repo: repository name :param sha: commit sha :param access_token: GitHub access token :return: repository data """ try: output = get_template( BASE_URL + owner + "/" + repo + "/commits/" + sha, access_token ) files = output["files"] return [RawCommitFile.model_validate(f) for f in files] except Exception: return None ================================================ FILE: backend/src/data/github/rest/models.py ================================================ from datetime import datetime from pydantic import BaseModel class RawCommit(BaseModel): timestamp: datetime node_id: str class RawCommitFile(BaseModel): filename: str additions: int deletions: int ================================================ FILE: backend/src/data/github/rest/repo.py ================================================ import logging from datetime import datetime from typing import Any, Dict, List, Optional from src.data.github.rest.models import RawCommit from src.data.github.rest.template import RESTError, get_template, get_template_plural BASE_URL = "https://api.github.com/repos/" # NOTE: unused, untested def get_repo(access_token: str, owner: str, repo: str) -> Dict[str, Any]: """ Returns raw repository data :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :return: repository data """ return get_template(BASE_URL + owner + "/" + repo, access_token) # NOTE: unused, untested def get_repo_languages( access_token: str, owner: str, repo: str ) -> List[Dict[str, Any]]: """ Returns repository language breakdown :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :return: repository language breakdown """ return get_template_plural( BASE_URL + owner + "/" + repo + "/languages", access_token ) def get_repo_stargazers( access_token: str, owner: str, repo: str, per_page: int = 100, page: int = 1 ) -> List[Dict[str, Any]]: """ Returns stargazers with timestamp for repository :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :param per_page: number of items per page :param page: page number :return: stargazers with timestamp for repository """ return get_template_plural( BASE_URL + owner + "/" + repo + "/stargazers", access_token, per_page=per_page, page=page, accept_header="applicaiton/vnd.github.v3.star+json", ) # NOTE: unused, untested # does not accept per page, exceeds if necessary def get_repo_code_frequency(access_token: str, owner: str, repo: str) -> Dict[str, Any]: """ Returns code frequency for repository :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :return: code frequency for repository """ return get_template( BASE_URL + owner + "/" + repo + "/stats/code_frequency", access_token ) # NOTE: unused, untested def get_repo_commit_activity( access_token: str, owner: str, repo: str ) -> Dict[str, Any]: """ Returns commit activity for past year, broken by week :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :return: commit activity for past year, broken by week """ return get_template( BASE_URL + owner + "/" + repo + "/stats/commit_activity", access_token ) # NOTE: unused, untested def get_repo_contributors(access_token: str, owner: str, repo: str) -> Dict[str, Any]: """ Returns contributors for a repository :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :return: contributors for a repository """ return get_template( BASE_URL + owner + "/" + repo + "/stats/contributors", access_token ) # NOTE: unused, untested def get_repo_weekly_commits(access_token: str, owner: str, repo: str) -> Dict[str, Any]: """ Returns contributions by week, owner/non-owner :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :return: contributions by week, owner/non-owner """ return get_template( BASE_URL + owner + "/" + repo + "/stats/participation", access_token ) # NOTE: unused, untested def get_repo_hourly_commits(access_token: str, owner: str, repo: str) -> Dict[str, Any]: """ Returns contributions by day, hour for repository :param access_token: GitHub access token :param owner: repository owner :param repo: repository name """ return get_template( BASE_URL + owner + "/" + repo + "/stats/punch_card", access_token ) def get_repo_commits( owner: str, repo: str, user: Optional[str] = None, since: Optional[datetime] = None, until: Optional[datetime] = None, page: int = 1, access_token: Optional[str] = None, ) -> List[RawCommit]: """ Returns most recent commits :param access_token: GitHub access token :param owner: repository owner :param repo: repository name :param user: optional GitHub user if not owner :param since: optional datetime to start from :param until: optional datetime to end at :param page: optional page number :return: Up to 100 commits from page """ user = user if user is not None else owner query = BASE_URL + owner + "/" + repo + "/commits?author=" + user if since is not None: query += f"&since={str(since)}" if until is not None: query += f"&until={str(until)}" try: data = get_template_plural(query, access_token, page=page) def extract_info(x: Any) -> RawCommit: dt = x["commit"]["committer"]["date"] temp = { "timestamp": datetime.strptime(dt, "%Y-%m-%dT%H:%M:%SZ"), "node_id": x["node_id"], } return RawCommit.model_validate(temp) return list(map(extract_info, data)) except RESTError: return [] except Exception as e: logging.exception(e) return [] ================================================ FILE: backend/src/data/github/rest/template.py ================================================ from datetime import datetime from typing import Any, Dict, List, Optional import requests from requests.exceptions import ReadTimeout from src.constants import TIMEOUT from src.data.github.utils import get_access_token s = requests.session() class RESTError(Exception): pass class RESTErrorUnauthorized(RESTError): pass class RESTErrorNotFound(RESTError): pass class RESTErrorEmptyRepo(RESTError): pass class RESTErrorTimeout(RESTError): pass def _get_template( query: str, params: Dict[str, Any], accept_header: str, access_token: Optional[str] = None, retries: int = 0, ) -> Any: """ Internal template for interacting with the GitHub REST API :param query: The query to be sent to the GitHub API :param params: The parameters to be sent to the GitHub API :param access_token: The access token to be sent to the GitHub API :param accept_header: The accept header to be sent to the GitHub API :return: The response from the GitHub API """ start = datetime.now() new_access_token = get_access_token(access_token) headers: Dict[str, str] = { "Accept": accept_header, "Authorization": f"bearer {new_access_token}", } try: r = s.get(query, params=params, headers=headers, timeout=TIMEOUT) except ReadTimeout: raise RESTErrorTimeout("REST Error: Request Timeout") if r.status_code == 200: print("REST API", new_access_token, datetime.now() - start) return r.json() if r.status_code == 401: raise RESTErrorUnauthorized("REST Error: Unauthorized") if r.status_code == 404: raise RESTErrorNotFound("REST Error: Not Found") if r.status_code == 409: raise RESTErrorEmptyRepo("REST Error: Empty Repository") if retries < 3: print("REST Error, Retrying:", new_access_token) return _get_template(query, params, accept_header, access_token, retries + 1) raise RESTError(f"REST Error: {str(r.status_code)}") def get_template( query: str, access_token: Optional[str] = None, accept_header: str = "application/vnd.github.v3+json", ) -> Dict[str, Any]: """ Template for interacting with the GitHub REST API (singular) :param query: The query to be sent to the GitHub API :param access_token: The access token to be sent to the GitHub API :param accept_header: The accept header to be sent to the GitHub API :return: The response from the GitHub API """ return _get_template(query, {}, accept_header, access_token) def get_template_plural( query: str, access_token: Optional[str] = None, per_page: int = 100, page: int = 1, accept_header: str = "application/vnd.github.v3+json", ) -> List[Dict[str, Any]]: """ Template for interacting with the GitHub REST API (plural) :param query: The query to be sent to the GitHub API :param access_token: The access token to be sent to the GitHub API :param per_page: The number of items to be returned per page :param page: The page number to be returned :param accept_header: The accept header to be sent to the GitHub API :return: The response from the GitHub API """ params: Dict[str, str] = {"per_page": str(per_page), "page": str(page)} return _get_template(query, params, accept_header, access_token) ================================================ FILE: backend/src/data/github/rest/user.py ================================================ from typing import Any, Dict, List from src.data.github.rest.template import get_template, get_template_plural BASE_URL = "https://api.github.com/users/" def get_user(user_id: str, access_token: str) -> Dict[str, Any]: """ Returns raw user data :param user_id: GitHub user id :param access_token: GitHub access token """ return get_template(BASE_URL + user_id, access_token) def get_user_starred_repos( user_id: str, access_token: str, per_page: int = 100, page: int = 1 ) -> List[Dict[str, Any]]: """ Returns list of starred repos :param user_id: GitHub user id :param access_token: GitHub access token :param per_page: number of repos to return per page """ return get_template_plural( BASE_URL + user_id + "/starred", access_token, per_page=per_page, page=page, accept_header="application/vnd.github.v3.star+json", ) ================================================ FILE: backend/src/data/github/utils.py ================================================ from typing import Optional from src.data.mongo.secret import get_random_key def get_access_token(access_token: Optional[str] = None) -> str: return access_token if access_token is not None else get_random_key() ================================================ FILE: backend/src/data/mongo/__init__.py ================================================ ================================================ FILE: backend/src/data/mongo/main.py ================================================ from motor.core import AgnosticCollection from motor.motor_asyncio import AsyncIOMotorClient from src.constants import LOCAL, MONGODB_PASSWORD, PROD def get_conn_str(password: str, database: str) -> str: return f"mongodb+srv://root:{password}@backend2.e50j8dp.mongodb.net/{database}?retryWrites=true&w=majority" if LOCAL: DB = None elif PROD: conn_str = get_conn_str(MONGODB_PASSWORD, "prod_backend") CLIENT = AsyncIOMotorClient( conn_str, serverSelectionTimeoutMS=5000, tlsInsecure=True ) DB = CLIENT.prod_backend # type: ignore else: conn_str = get_conn_str(MONGODB_PASSWORD, "dev_backend") CLIENT = AsyncIOMotorClient( # type: ignore conn_str, serverSelectionTimeoutMS=5000, tlsInsecure=True ) DB = CLIENT.dev_backend # type: ignore # Overwrite type since only None if Local=True SECRETS: AgnosticCollection = None if DB is None else DB.secrets # type: ignore USERS: AgnosticCollection = None if DB is None else DB.users # type: ignore USER_MONTHS: AgnosticCollection = None if DB is None else DB.user_months # type: ignore ================================================ FILE: backend/src/data/mongo/secret/__init__.py ================================================ from src.data.mongo.secret.functions import get_random_key, update_keys __all__ = ["get_random_key", "update_keys"] ================================================ FILE: backend/src/data/mongo/secret/functions.py ================================================ from datetime import timedelta from random import randint from typing import Any, Dict, List, Optional, Tuple from src.constants import TEST_TOKEN from src.data.mongo.main import SECRETS from src.data.mongo.secret.models import SecretModel from src.utils import alru_cache @alru_cache(ttl=timedelta(minutes=15)) async def get_keys(no_cache: bool = False) -> Tuple[bool, List[str]]: secrets: Optional[Dict[str, Any]] = await SECRETS.find_one({"project": "main"}) if secrets is None: return (False, []) tokens = SecretModel.model_validate(secrets).access_tokens return (True, tokens) secret_keys: List[str] = [] async def update_keys(no_cache: bool = False) -> None: global secret_keys secret_keys = await get_keys(no_cache=no_cache) def get_random_key() -> str: global secret_keys if len(secret_keys) == 0: return TEST_TOKEN return secret_keys[randint(0, len(secret_keys) - 1)] ================================================ FILE: backend/src/data/mongo/secret/models.py ================================================ from typing import List from pydantic import BaseModel class SecretModel(BaseModel): project: str access_tokens: List[str] ================================================ FILE: backend/src/data/mongo/user/__init__.py ================================================ from src.data.mongo.user.functions import delete_user, is_user_key, update_user from src.data.mongo.user.get import get_full_user, get_public_user from src.data.mongo.user.models import FullUserModel, PublicUserModel __all__ = [ "delete_user", "is_user_key", "update_user", "get_full_user", "get_public_user", "FullUserModel", "PublicUserModel", ] ================================================ FILE: backend/src/data/mongo/user/functions.py ================================================ from typing import Any, Dict, Optional from src.data.mongo.main import USERS async def is_user_key(user_id: str, user_key: str) -> bool: user: Optional[dict[str, str]] = await USERS.find_one( {"user_id": user_id}, {"user_key": 1} ) return user is not None and user.get("user_key", "") == user_key async def update_user(user_id: str, raw_user: Dict[str, Any]) -> None: await USERS.update_one( {"user_id": user_id}, {"$set": raw_user}, upsert=True, ) async def delete_user(user_id: str, user_key: str, use_user_key: bool = True) -> bool: if use_user_key: is_key = await is_user_key(user_id, user_key) if not is_key: return False await USERS.delete_one({"user_id": user_id}) return True ================================================ FILE: backend/src/data/mongo/user/get.py ================================================ from typing import Any, Dict, Optional, Tuple from pydantic import ValidationError from src.data.mongo.main import USERS from src.data.mongo.user.models import FullUserModel, PublicUserModel from src.utils import alru_cache @alru_cache() async def get_public_user( user_id: str, no_cache: bool = False ) -> Tuple[bool, Optional[PublicUserModel]]: user: Optional[Dict[str, Any]] = await USERS.find_one({"user_id": user_id}) if user is None: # flag is false, don't cache return (False, None) try: return (True, PublicUserModel.model_validate(user)) except (TypeError, KeyError, ValidationError): return (False, None) @alru_cache() async def get_full_user( user_id: str, no_cache: bool = False ) -> Tuple[bool, Optional[FullUserModel]]: user: Optional[Dict[str, Any]] = await USERS.find_one({"user_id": user_id}) if user is None: # flag is false, don't cache return (False, None) try: return (True, FullUserModel.model_validate(user)) except (TypeError, KeyError, ValidationError): return (False, None) ================================================ FILE: backend/src/data/mongo/user/models.py ================================================ from typing import Optional from pydantic import BaseModel, validator class PublicUserModel(BaseModel): user_id: str access_token: str private_access: Optional[bool] class Config: from_attributes = True validate_assignment = True @validator("private_access", pre=True, always=True) def set_name(cls, private_access: Optional[bool]): return False if private_access is None else private_access class FullUserModel(PublicUserModel): user_key: Optional[str] ================================================ FILE: backend/src/data/mongo/user_months/__init__.py ================================================ from src.data.mongo.user_months.functions import set_user_month from src.data.mongo.user_months.get import get_user_months from src.data.mongo.user_months.models import UserMonth __all__ = ["set_user_month", "get_user_months", "UserMonth"] ================================================ FILE: backend/src/data/mongo/user_months/functions.py ================================================ from src.data.mongo.main import USER_MONTHS from src.data.mongo.user_months.models import UserMonth async def set_user_month(user_month: UserMonth): compressed_user_month = user_month.model_dump() compressed_user_month["data"] = user_month.data.compress() await USER_MONTHS.update_one( {"user_id": user_month.user_id, "month": user_month.month}, {"$set": compressed_user_month}, upsert=True, ) ================================================ FILE: backend/src/data/mongo/user_months/get.py ================================================ from datetime import date, datetime from typing import Any, Dict, List from src.constants import API_VERSION, USER_WHITELIST from src.data.mongo.main import USER_MONTHS from src.data.mongo.user_months.models import UserMonth from src.models import UserPackage async def get_user_months( user_id: str, private_access: bool, start_month: date, end_month: date ) -> List[UserMonth]: start = datetime(start_month.year, start_month.month, 1) end = datetime(end_month.year, end_month.month, 28) today = datetime.now() filters = { "user_id": user_id, "month": {"$gte": start, "$lte": end}, "version": API_VERSION, } if private_access: filters["private"] = True months: List[Dict[str, Any]] = await USER_MONTHS.find(filters).to_list(length=None) # type: ignore months_data: List[UserMonth] = [] for month in months: date_obj: datetime = month["month"] complete = ( not (date_obj.year == today.year and date_obj.month == today.month) or user_id in USER_WHITELIST ) try: data = UserPackage.decompress(month["data"]) months_data.append( UserMonth.model_validate( { "user_id": user_id, "month": month["month"], "version": API_VERSION, "private": month["private"], "complete": complete, "data": data.model_dump(), } ) ) except Exception: pass return months_data ================================================ FILE: backend/src/data/mongo/user_months/models.py ================================================ from datetime import datetime from pydantic import BaseModel from src.models import UserPackage class UserMonth(BaseModel): user_id: str month: datetime version: float private: bool complete: bool data: UserPackage ================================================ FILE: backend/src/main.py ================================================ from typing import Dict import sentry_sdk from dotenv import find_dotenv, load_dotenv from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sentry_sdk.integrations.asgi import SentryAsgiMiddleware load_dotenv(find_dotenv()) # flake8: noqa E402 # add endpoints here (after load dotenv) from src.constants import PROD, SENTRY_DSN from src.routers import ( asset_router, auth_router, dev_router, user_router, wrapped_router, ) """ SETUP """ app = FastAPI() origins = [ "http://localhost:3000", "http://localhost:3001", "https://githubtrends.io", "https://www.githubtrends.io", "https://githubwrapped.io", "https://www.githubwrapped.io", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) sentry_sdk.init( SENTRY_DSN, traces_sample_rate=(1.0 if PROD else 1.0), ) app.add_middleware( SentryAsgiMiddleware, ) @app.get("/") async def read_root() -> Dict[str, str]: return {"Hello": "World"} @app.get("/info") def get_info() -> Dict[str, bool]: return {"PROD": PROD} app.include_router(asset_router, prefix="/assets", tags=["Assets"]) app.include_router(auth_router, prefix="/auth", tags=["Auth"]) app.include_router(dev_router, prefix="/dev", tags=["Dev"]) app.include_router(user_router, prefix="/user", tags=["Users"]) app.include_router(wrapped_router, prefix="/wrapped", tags=["Wrapped"]) ================================================ FILE: backend/src/models/__init__.py ================================================ from src.models.user.contribs import ( ContributionDay, Language, RepoContributionStats, UserContributions, ) from src.models.user.follows import User, UserFollows from src.models.user.main import UserPackage from src.models.wrapped.calendar import ( CalendarData, CalendarDayDatum, CalendarLanguageDayDatum, ) from src.models.wrapped.langs import LangData, LangDatum from src.models.wrapped.main import WrappedPackage from src.models.wrapped.numeric import ContribStats, LOCStats, MiscStats, NumericData from src.models.wrapped.repos import RepoData, RepoDatum from src.models.wrapped.time import DayData, MonthData, TimeDatum from src.models.wrapped.timestamps import TimestampData, TimestampDatum __all__ = [ "ContributionDay", "Language", "RepoContributionStats", "UserContributions", "User", "UserFollows", "UserPackage", "CalendarData", "CalendarDayDatum", "CalendarLanguageDayDatum", "LangData", "LangDatum", "WrappedPackage", "ContribStats", "LOCStats", "MiscStats", "NumericData", "RepoData", "RepoDatum", "DayData", "MonthData", "TimeDatum", "TimestampData", "TimestampDatum", ] ================================================ FILE: backend/src/models/background.py ================================================ from datetime import date from typing import Optional from pydantic import BaseModel class UpdateUserBackgroundTask(BaseModel): user_id: str access_token: Optional[str] private_access: bool start_date: Optional[date] end_date: Optional[date] ================================================ FILE: backend/src/models/svg.py ================================================ from typing import List, Optional from pydantic import BaseModel class LanguageStats(BaseModel): lang: str loc: int percent: float color: Optional[str] class RepoLanguage(BaseModel): lang: str color: Optional[str] loc: int class RepoStats(BaseModel): repo: str private: bool langs: List[RepoLanguage] loc: int ================================================ FILE: backend/src/models/user/__init__.py ================================================ ================================================ FILE: backend/src/models/user/contribs.py ================================================ from datetime import date, datetime from typing import Any, Dict, List, Optional, Tuple from pydantic import BaseModel class Language(BaseModel): color: Optional[str] additions: int deletions: int def compress(self) -> List[Any]: return [self.color, self.additions, self.deletions] @classmethod def decompress(cls, data: List[Any]) -> "Language": return Language(color=data[0], additions=data[1], deletions=data[2]) def __add__(self, other: "Language") -> "Language": return Language( color=self.color, additions=self.additions + other.additions, deletions=self.deletions + other.deletions, ) class ContributionStats(BaseModel): contribs_count: int commits_count: int issues_count: int prs_count: int reviews_count: int repos_count: int other_count: int languages: Dict[str, Language] def compress(self) -> List[Any]: out: List[Any] = [ [ self.contribs_count, self.commits_count, self.issues_count, self.prs_count, self.reviews_count, self.repos_count, self.other_count, ], *[[name] + stats.compress() for name, stats in self.languages.items()], ] return out @classmethod def decompress(cls, data: List[Any]) -> "ContributionStats": return ContributionStats( contribs_count=data[0][0], commits_count=data[0][1], issues_count=data[0][2], prs_count=data[0][3], reviews_count=data[0][4], repos_count=data[0][5], other_count=data[0][6], languages={x[0]: Language.decompress(x[1:]) for x in data[1:]}, ) def __add__(self, other: "ContributionStats") -> "ContributionStats": languages = self.languages for lang, lang_obj in other.languages.items(): if lang in languages: languages[lang] += lang_obj else: languages[lang] = lang_obj return ContributionStats( contribs_count=self.contribs_count + other.contribs_count, commits_count=self.commits_count + other.commits_count, issues_count=self.issues_count + other.issues_count, prs_count=self.prs_count + other.prs_count, reviews_count=self.reviews_count + other.reviews_count, repos_count=self.repos_count + other.repos_count, other_count=self.other_count + other.other_count, languages=languages, ) @classmethod def empty(cls) -> "ContributionStats": return ContributionStats( contribs_count=0, commits_count=0, issues_count=0, prs_count=0, reviews_count=0, repos_count=0, other_count=0, languages={}, ) class ContributionLists(BaseModel): commits: List[datetime] issues: List[datetime] prs: List[datetime] reviews: List[datetime] repos: List[datetime] def compress(self) -> List[Any]: return [self.commits, self.issues, self.prs, self.reviews, self.repos] @classmethod def decompress(cls, data: List[Any]) -> "ContributionLists": return ContributionLists( commits=data[0], issues=data[1], prs=data[2], reviews=data[3], repos=data[4] ) class ContributionDay(BaseModel): date: str weekday: int stats: ContributionStats lists: ContributionLists def compress(self) -> List[Any]: return [ self.date, self.weekday, self.stats.compress(), self.lists.compress(), ] @classmethod def decompress(cls, data: List[Any]) -> "ContributionDay": return ContributionDay( date=data[0], weekday=data[1], stats=ContributionStats.decompress(data[2]), lists=ContributionLists.decompress(data[3]), ) class RepoContributionStats(ContributionStats, BaseModel): private: bool contribs_count: int commits_count: int issues_count: int prs_count: int reviews_count: int repos_count: int other_count: int languages: Dict[str, Language] def compress(self) -> List[Any]: out = super().compress() out[0].append(self.private) return out @classmethod def decompress(cls, data: List[Any]) -> "RepoContributionStats": contribs = super().decompress(data).model_dump() contribs["private"] = data[0][7] return RepoContributionStats(**contribs) def __add__( # type: ignore self, other: "RepoContributionStats" ) -> "RepoContributionStats": new_self = ContributionStats(**self.model_dump()) new_other = ContributionStats(**other.model_dump()) combined = (new_self + new_other).model_dump() combined["private"] = self.private return RepoContributionStats(**combined) class UserContributions(BaseModel): total_stats: ContributionStats public_stats: ContributionStats total: List[ContributionDay] public: List[ContributionDay] repo_stats: Dict[str, RepoContributionStats] repos: Dict[str, List[ContributionDay]] def compress(self) -> List[Any]: new_total_stats = self.total_stats.compress() new_public_stats = self.public_stats.compress() new_total = [x.compress() for x in self.total] new_public = [x.compress() for x in self.public] new_repo_stats = {k: v.compress() for k, v in self.repo_stats.items()} new_repos = {k: [x.compress() for x in v] for k, v in self.repos.items()} return [ new_total_stats, new_public_stats, new_total, new_public, new_repo_stats, new_repos, ] @classmethod def decompress(cls, data: List[Any]) -> "UserContributions": total_stats = ContributionStats.decompress(data[0]) public_stats = ContributionStats.decompress(data[1]) total = [ContributionDay.decompress(x) for x in data[2]] public = [ContributionDay.decompress(x) for x in data[3]] repo_stats = { k: RepoContributionStats.decompress(v) for k, v in data[4].items() } repos = { k: [ContributionDay.decompress(x) for x in v] for k, v in data[5].items() } return UserContributions( total_stats=total_stats, public_stats=public_stats, total=total, public=public, repo_stats=repo_stats, repos=repos, ) def __add__(self, other: "UserContributions") -> "UserContributions": new_total_stats = self.total_stats + other.total_stats new_public_stats = self.public_stats + other.public_stats new_total = sorted(self.total + other.total, key=lambda x: x.date) new_public = sorted(self.public + other.public, key=lambda x: x.date) new_repo_stats = self.repo_stats for repo, stats in other.repo_stats.items(): if repo in new_repo_stats: new_repo_stats[repo] += stats else: new_repo_stats[repo] = stats new_repos = self.repos for repo, days in other.repos.items(): if repo in new_repos: new_repos[repo] = sorted(new_repos[repo] + days, key=lambda x: x.date) else: new_repos[repo] = days return UserContributions( total_stats=new_total_stats, public_stats=new_public_stats, total=new_total, public=new_public, repo_stats=new_repo_stats, repos=new_repos, ) @staticmethod def trim_contribs( contribs: List[ContributionDay], start_date: date, end_date: date ) -> Tuple[List[ContributionDay], ContributionStats]: new_total: List[ContributionDay] = [] for day in contribs: curr_date = datetime.strptime(day.date, "%Y-%m-%d").date() if curr_date >= start_date and curr_date <= end_date: new_total.append(day) new_languages: Dict[str, Language] = {} for day in new_total: for lang in day.stats.languages: if lang in new_languages: new_languages[lang] += day.stats.languages[lang] else: new_languages[lang] = day.stats.languages[lang] new_stats = ContributionStats( contribs_count=sum(x.stats.contribs_count for x in new_total), commits_count=sum(x.stats.commits_count for x in new_total), issues_count=sum(x.stats.issues_count for x in new_total), prs_count=sum(x.stats.prs_count for x in new_total), reviews_count=sum(x.stats.reviews_count for x in new_total), repos_count=sum(x.stats.repos_count for x in new_total), other_count=sum(x.stats.other_count for x in new_total), languages=new_languages, ) return new_total, new_stats def trim(self, start: date, end: date) -> "UserContributions": new_total, new_total_stats = self.trim_contribs(self.total, start, end) new_public, new_public_stats = self.trim_contribs(self.public, start, end) new_repos_dict: Dict[str, List[ContributionDay]] = {} new_repo_stats_dict: Dict[str, RepoContributionStats] = {} for repo_name, repo in self.repos.items(): new_repo_total, _new_repo_stats = self.trim_contribs(repo, start, end) if len(new_repo_total) > 0: new_repos_dict[repo_name] = new_repo_total raw_new_repo_stats = _new_repo_stats.model_dump() raw_new_repo_stats["private"] = self.repo_stats[repo_name].private new_repo_stats = RepoContributionStats(**raw_new_repo_stats) new_repo_stats_dict[repo_name] = new_repo_stats return UserContributions( total_stats=new_total_stats, public_stats=new_public_stats, total=new_total, public=new_public, repo_stats=new_repo_stats_dict, repos=new_repos_dict, ) @classmethod def empty(cls) -> "UserContributions": return UserContributions( total_stats=ContributionStats.empty(), public_stats=ContributionStats.empty(), total=[], public=[], repo_stats={}, repos={}, ) ================================================ FILE: backend/src/models/user/follows.py ================================================ from typing import List, Optional from pydantic import BaseModel class User(BaseModel): name: Optional[str] login: str url: str class Config: allow_none = True class UserFollows(BaseModel): followers: List[User] following: List[User] ================================================ FILE: backend/src/models/user/main.py ================================================ from datetime import date from typing import Any, Dict from pydantic import BaseModel from src.models.user.contribs import UserContributions # from src.models.user.follows import UserFollows class UserPackage(BaseModel): contribs: UserContributions incomplete: bool = False def compress(self): return { "c": self.contribs.compress(), } @classmethod def decompress(cls, data: Dict[str, Any]) -> "UserPackage": return UserPackage( contribs=UserContributions.decompress(data["c"]), ) def __add__(self, other: "UserPackage") -> "UserPackage": return UserPackage(contribs=self.contribs + other.contribs) def trim(self, start: date, end: date) -> "UserPackage": return UserPackage(contribs=self.contribs.trim(start, end)) @classmethod def empty(cls) -> "UserPackage": return UserPackage(contribs=UserContributions.empty()) ================================================ FILE: backend/src/models/wrapped/__init__.py ================================================ ================================================ FILE: backend/src/models/wrapped/calendar.py ================================================ from typing import Dict, List from pydantic import BaseModel class CalendarLanguageDayDatum(BaseModel): loc_added: int loc_changed: int class CalendarDayDatum(BaseModel): day: str contribs: int commits: int issues: int prs: int reviews: int loc_added: int loc_changed: int top_langs: Dict[str, CalendarLanguageDayDatum] class CalendarData(BaseModel): days: List[CalendarDayDatum] @classmethod def empty(cls) -> "CalendarData": return CalendarData(days=[]) ================================================ FILE: backend/src/models/wrapped/langs.py ================================================ from typing import List from pydantic import BaseModel class LangDatum(BaseModel): id: str label: str value: int formatted_value: str color: str class LangData(BaseModel): langs_changed: List[LangDatum] langs_added: List[LangDatum] @classmethod def empty(cls) -> "LangData": return LangData(langs_changed=[], langs_added=[]) ================================================ FILE: backend/src/models/wrapped/main.py ================================================ from pydantic import BaseModel from src.models.wrapped.calendar import CalendarData from src.models.wrapped.langs import LangData from src.models.wrapped.numeric import NumericData from src.models.wrapped.repos import RepoData from src.models.wrapped.time import DayData, MonthData from src.models.wrapped.timestamps import TimestampData class WrappedPackage(BaseModel): month_data: MonthData day_data: DayData calendar_data: CalendarData numeric_data: NumericData repo_data: RepoData lang_data: LangData timestamp_data: TimestampData incomplete: bool = False @classmethod def empty(cls) -> "WrappedPackage": return WrappedPackage( month_data=MonthData.empty(), day_data=DayData.empty(), calendar_data=CalendarData.empty(), numeric_data=NumericData.empty(), repo_data=RepoData.empty(), lang_data=LangData.empty(), timestamp_data=TimestampData.empty(), ) ================================================ FILE: backend/src/models/wrapped/numeric.py ================================================ from typing import Optional, Tuple from pydantic import BaseModel class ContribStats(BaseModel): contribs: int commits: int issues: int prs: int reviews: int other: int @classmethod def empty(cls) -> "ContribStats": return ContribStats( contribs=0, commits=0, issues=0, prs=0, reviews=0, other=0, ) class MiscStats(BaseModel): total_days: int longest_streak: int longest_streak_days: Tuple[int, int, str, str] longest_gap: int longest_gap_days: Tuple[int, int, str, str] weekend_percent: int best_day_count: int best_day_date: Optional[str] best_day_index: Optional[int] @classmethod def empty(cls) -> "MiscStats": return MiscStats( total_days=0, longest_streak=0, longest_streak_days=(0, 0, "", ""), longest_gap=0, longest_gap_days=(0, 0, "", ""), weekend_percent=0, best_day_count=0, best_day_date=None, best_day_index=None, ) class LOCStats(BaseModel): loc_additions: str loc_deletions: str loc_changed: str loc_added: str loc_additions_per_commit: int loc_deletions_per_commit: int loc_changed_per_day: int @classmethod def empty(cls) -> "LOCStats": return LOCStats( loc_additions="0", loc_deletions="0", loc_changed="0", loc_added="0", loc_additions_per_commit=0, loc_deletions_per_commit=0, loc_changed_per_day=0, ) class NumericData(BaseModel): contribs: ContribStats misc: MiscStats loc: LOCStats @classmethod def empty(cls) -> "NumericData": return NumericData( contribs=ContribStats.empty(), misc=MiscStats.empty(), loc=LOCStats.empty(), ) ================================================ FILE: backend/src/models/wrapped/repos.py ================================================ from typing import List from pydantic import BaseModel class RepoDatum(BaseModel): id: int label: str value: int formatted_value: str class RepoData(BaseModel): repos_changed: List[RepoDatum] repos_added: List[RepoDatum] @classmethod def empty(cls) -> "RepoData": return RepoData(repos_changed=[], repos_added=[]) ================================================ FILE: backend/src/models/wrapped/time.py ================================================ from typing import List from pydantic import BaseModel class TimeDatum(BaseModel): index: int contribs: int loc_changed: int formatted_loc_changed: str class MonthData(BaseModel): months: List[TimeDatum] @classmethod def empty(cls) -> "MonthData": return MonthData(months=[]) class DayData(BaseModel): days: List[TimeDatum] @classmethod def empty(cls) -> "DayData": return DayData(days=[]) ================================================ FILE: backend/src/models/wrapped/timestamps.py ================================================ from typing import List from pydantic import BaseModel class TimestampDatum(BaseModel): type: str weekday: int timestamp: int class TimestampData(BaseModel): contribs: List[TimestampDatum] @classmethod def empty(cls) -> "TimestampData": return TimestampData(contribs=[]) ================================================ FILE: backend/src/processing/auth.py ================================================ from typing import Any, Dict, Optional, Tuple from src.data.github.auth import authenticate as github_authenticate from src.data.mongo.user import ( PublicUserModel, delete_user as db_delete_user, get_public_user as db_get_public_user, update_user as db_update_user, ) from src.models.background import UpdateUserBackgroundTask # frontend first calls set_user_key with code and user_key # next they call authenticate which determines the user_id to associate with the code/user_key # these actions should happen sequentially, so in-memory storage is fine code_key_map: Dict[str, str] = {} async def set_user_key(code: str, user_key: str) -> str: code_key_map[code] = user_key return user_key async def authenticate( code: str, private_access: bool ) -> Tuple[str, Optional[UpdateUserBackgroundTask]]: user_id, access_token = await github_authenticate(code) curr_user: Optional[PublicUserModel] = await db_get_public_user(user_id) raw_user: Dict[str, Any] = { "user_id": user_id, "access_token": access_token, "user_key": code_key_map.get(code, None), "private_access": private_access, } background_task = None if curr_user is not None: curr_private_access = curr_user.private_access new_private_access = curr_private_access or private_access raw_user["private_access"] = new_private_access if new_private_access != curr_private_access: # new private access status background_task = UpdateUserBackgroundTask( user_id=user_id, access_token=access_token, private_access=new_private_access, start_date=None, end_date=None, ) else: # first time sign up background_task = UpdateUserBackgroundTask( user_id=user_id, access_token=access_token, private_access=private_access, start_date=None, end_date=None, ) await db_update_user(user_id, raw_user) return user_id, background_task async def delete_user(user_id: str, user_key: str, use_user_key: bool = True) -> bool: return await db_delete_user(user_id, user_key, use_user_key) ================================================ FILE: backend/src/processing/user/__init__.py ================================================ from src.processing.user.commits import get_top_languages, get_top_repos from src.processing.user.svg import svg_base __all__ = ["get_top_languages", "get_top_repos", "svg_base"] ================================================ FILE: backend/src/processing/user/commits.py ================================================ from typing import Any, Dict, List, Optional, Tuple, Union from src.constants import DEFAULT_COLOR from src.models import UserPackage from src.models.svg import LanguageStats, RepoStats dict_type = Dict[str, Union[str, int, float]] def loc_metric_func(loc_metric: str, additions: int, deletions: int) -> int: if loc_metric == "changed": return additions + deletions return additions - deletions def get_top_languages( data: UserPackage, loc_metric: str, include_private: bool ) -> Tuple[List[LanguageStats], int]: raw_languages = ( data.contribs.total_stats.languages if include_private else data.contribs.public_stats.languages ) languages_list = [ LanguageStats( lang=lang, color=stats.color or DEFAULT_COLOR, loc=loc_metric_func(loc_metric, stats.additions, stats.deletions), percent=-1, ) for lang, stats in raw_languages.items() ] languages_list = list(filter(lambda x: x.loc > 0, languages_list)) total_loc = sum(x.loc for x in languages_list) + 1 total = LanguageStats(lang="Total", color=None, loc=total_loc, percent=100) languages_list = sorted(languages_list, key=lambda x: x.loc, reverse=True) other = LanguageStats(lang="Other", color="#ededed", loc=0, percent=-1) for language in languages_list[4:]: other.loc = other.loc + language.loc languages_list = [total] + languages_list[:4] + [other] new_languages_list: List[LanguageStats] = [] for lang in languages_list: lang.percent = float(round(100 * lang.loc / total_loc, 2)) if lang.percent > 1: # 1% minimum to show new_languages_list.append(LanguageStats.model_validate(lang)) commits_excluded = data.contribs.public_stats.other_count if include_private: commits_excluded = data.contribs.total_stats.other_count return new_languages_list, commits_excluded def get_top_repos( data: UserPackage, loc_metric: str, include_private: bool, group: str ) -> Tuple[List[RepoStats], int]: repos: List[Any] = [ { "repo": repo, "private": repo_stats.private, "langs": [ { "lang": x[0], "color": x[1].color, "loc": loc_metric_func(loc_metric, x[1].additions, x[1].deletions), } for x in list(repo_stats.languages.items()) ], } for repo, repo_stats in data.contribs.repo_stats.items() if include_private or not repo_stats.private ] for repo in repos: repo["loc"] = sum(x["loc"] for x in repo["langs"]) # first estimate repos = list(filter(lambda x: x["loc"] > 0, repos)) for repo in repos: repo["langs"] = [x for x in repo["langs"] if x["loc"] > 0.05 * repo["loc"]] repo["loc"] = sum(x["loc"] for x in repo["langs"]) # final estimate repos = sorted(repos, key=lambda x: x["loc"], reverse=True) new_repos = [ RepoStats.model_validate(x) for x in repos if x["loc"] > 0.01 * repos[0]["loc"] ] commits_excluded = data.contribs.public_stats.other_count if include_private: commits_excluded = data.contribs.total_stats.other_count # With n bars, group from n onwards into the last bar bars = 4 # TODO: make this configurable (see issues) if group == "none" or len(new_repos) <= bars: return new_repos[:bars], commits_excluded out_repos = [] other_repos = [] if group == "other": out_repos = new_repos[: bars - 1] other_repos = new_repos[bars - 1 :] elif group == "private": public_repos = [x for x in new_repos if not x.private] private_repos = [x for x in new_repos if x.private] if len(public_repos) < 4 and len(private_repos) > 0: public_repos += private_repos[: bars - len(public_repos) - 1] private_repos = private_repos[bars - len(public_repos) - 1 :] out_repos = sorted(public_repos[: bars - 1], key=lambda x: x.loc, reverse=True) other_repos = public_repos[bars - 1 :] + private_repos else: raise ValueError("Invalid group value") other: Dict[str, Tuple[int, Optional[str]]] = {} for repo in other_repos: for _lang in repo.langs: lang = _lang.lang if lang not in other: other[lang] = (0, _lang.color) other[lang] = (other[lang][0] + _lang.loc, other[lang][1]) out_repos.append( RepoStats( repo="other/repos", private=False, langs=[{"lang": k, "loc": v[0], "color": v[1]} for k, v in other.items()], # type: ignore loc=sum(v[0] for v in other.values()), ) ) return out_repos, commits_excluded ================================================ FILE: backend/src/processing/user/svg.py ================================================ from datetime import date from typing import Optional, Tuple from src.aggregation.layer2.user import get_user, get_user_demo from src.models import UserPackage from src.models.background import UpdateUserBackgroundTask from src.utils import use_time_range async def svg_base( user_id: str, start_date: date, end_date: date, time_range: str, demo: bool, no_cache: bool = False, ) -> Tuple[Optional[UserPackage], bool, Optional[UpdateUserBackgroundTask], str]: # process time_range, start_date, end_date time_range = "one_month" if demo else time_range start_date, end_date, time_str = use_time_range(time_range, start_date, end_date) complete = True # overwritten later if not complete background_task = None # fetch data, either using demo or user method if demo: output = await get_user_demo(user_id, start_date, end_date, no_cache=no_cache) else: output, complete, background_task = await get_user( user_id, start_date, end_date, no_cache=no_cache ) return output, complete, background_task, time_str ================================================ FILE: backend/src/processing/wrapped/__init__.py ================================================ from src.processing.wrapped.main import query_wrapped_user __all__ = ["query_wrapped_user"] ================================================ FILE: backend/src/processing/wrapped/calendar.py ================================================ from datetime import datetime, timedelta from typing import Any, Dict, List from src.models import CalendarData, CalendarDayDatum, UserPackage def get_calendar_data(data: UserPackage, year: int) -> CalendarData: top_langs = [ x[0] for x in sorted( data.contribs.total_stats.languages.items(), key=lambda x: x[1].additions + x[1].deletions, reverse=True, )[:5] ] total_out: List[CalendarDayDatum] = [] items_dict = {item.date: item for item in data.contribs.total} for i in range(365): date = (datetime(year, 1, 1) + timedelta(days=i - 1)).strftime("%Y-%m-%d") item = items_dict.get(date) out: Dict[str, Any] = { "day": date, "contribs": 0, "commits": 0, "issues": 0, "prs": 0, "reviews": 0, "loc_added": 0, "loc_changed": 0, "top_langs": {k: {"loc_added": 0, "loc_changed": 0} for k in top_langs}, } if item is not None: out["contribs"] = item.stats.contribs_count out["commits"] = item.stats.commits_count out["issues"] = item.stats.issues_count out["prs"] = item.stats.prs_count out["reviews"] = item.stats.reviews_count for k, v in item.stats.languages.items(): if k in top_langs: out["top_langs"][k]["loc_added"] = v.additions - v.deletions out["top_langs"][k]["loc_changed"] = v.additions + v.deletions out["loc_added"] += v.additions - v.deletions out["loc_changed"] += v.additions + v.deletions out_obj = CalendarDayDatum.model_validate(out) total_out.append(out_obj) return CalendarData.model_validate({"days": total_out}) ================================================ FILE: backend/src/processing/wrapped/langs.py ================================================ from typing import List from src.constants import DEFAULT_COLOR from src.models import LangData, LangDatum, Language, UserPackage from src.utils import format_number def _count_loc(x: Language, metric: str) -> int: if metric == "changed": return x.additions + x.deletions return x.additions - x.deletions def get_lang_data(data: UserPackage) -> LangData: out = {} for m in ["changed", "added"]: langs = sorted( data.contribs.total_stats.languages.items(), key=lambda x: _count_loc(x[1], m), reverse=True, ) lang_objs: List[LangDatum] = [] for k, v in list(langs)[:5]: lang_data = { "id": k, "label": k, "value": _count_loc(v, m), "formatted_value": format_number(_count_loc(v, m)), "color": v.color, } lang_objs.append(LangDatum.model_validate(lang_data)) # remaining languages total_count = sum(_count_loc(v, m) for _, v in list(langs)[5:]) lang_data = { "id": "other", "label": "other", "value": total_count, "formatted_value": format_number(total_count), "color": DEFAULT_COLOR, } if total_count > 100: lang_objs.append(LangDatum.model_validate(lang_data)) out[f"langs_{m}"] = lang_objs return LangData.model_validate(out) ================================================ FILE: backend/src/processing/wrapped/main.py ================================================ from datetime import date, timedelta from typing import Optional, Tuple from src.aggregation.layer1 import query_user from src.data.mongo.user import PublicUserModel, get_public_user as db_get_public_user from src.models import UserPackage, WrappedPackage from src.processing.wrapped.package import get_wrapped_data from src.utils import alru_cache @alru_cache(ttl=timedelta(hours=12)) async def query_wrapped_user( user_id: str, year: int, no_cache: bool = False ) -> Tuple[bool, Optional[WrappedPackage]]: start_date, end_date = date(year, 1, 1), date(year, 12, 31) user: Optional[PublicUserModel] = await db_get_public_user(user_id) private_access = False access_token = None if user is not None and user.private_access: private_access = True access_token = user.access_token user_package: UserPackage = await query_user( user_id, access_token, private_access, start_date, end_date, max_time=40, no_cache=True, ) wrapped_package = get_wrapped_data(user_package, year) # Don't cache if incomplete return (not wrapped_package.incomplete, wrapped_package) ================================================ FILE: backend/src/processing/wrapped/numeric.py ================================================ from collections import defaultdict from datetime import datetime from typing import Dict from src.models import ContribStats, LOCStats, MiscStats, NumericData, UserPackage def get_contrib_stats(data: UserPackage) -> ContribStats: return ContribStats.model_validate( { "contribs": data.contribs.total_stats.contribs_count, "commits": data.contribs.total_stats.commits_count, "issues": data.contribs.total_stats.issues_count, "prs": data.contribs.total_stats.prs_count, "reviews": data.contribs.total_stats.reviews_count, "other": data.contribs.total_stats.other_count, } ) def get_misc_stats(data: UserPackage, year: int) -> MiscStats: weekdays: Dict[int, int] = defaultdict(int) yeardays, distinct_days, total_contribs = {}, 0, 0 for item in data.contribs.total: count = item.stats.contribs_count weekdays[item.weekday] += count total_contribs += item.stats.contribs_count if count > 0: date = datetime.fromisoformat(item.date) yeardays[date.timetuple().tm_yday - 1] = 1 distinct_days += 1 curr, best, best_dates = 0, 0, (1, 1) for i in range(366): curr = curr + 1 if i in yeardays else 0 if curr > best: best = curr best_dates = (i - curr + 2, i + 1) longest_streak = max(best, curr) longest_streak_days = ( best_dates[0], best_dates[1], datetime.fromordinal(max(1, best_dates[0])).strftime("%b %d"), datetime.fromordinal(max(1, best_dates[1])).strftime("%b %d"), ) curr, best, best_dates = 0, 0, (1, 1) days = (datetime.now() - datetime(year, 1, 1)).days for i in range(min(days, 365)): curr = 0 if i in yeardays else curr + 1 if curr > best: best = curr best_dates = (i - curr + 2, i + 1) longest_gap = max(best, curr) longest_gap_days = ( best_dates[0], best_dates[1], datetime.fromordinal(max(1, best_dates[0])).strftime("%b %d"), datetime.fromordinal(max(1, best_dates[1])).strftime("%b %d"), ) weekend_percent = (weekdays[0] + weekdays[6]) / max(1, total_contribs) best_day_count, best_day_date, best_day_index = 0, None, None if len(data.contribs.total) > 0: best_day = max(data.contribs.total, key=lambda x: x.stats.contribs_count) best_day_index = datetime.fromisoformat(best_day.date).timetuple().tm_yday best_day_count = best_day.stats.contribs_count best_day_date = best_day.date return MiscStats.model_validate( { "total_days": distinct_days, "longest_streak": longest_streak, "longest_streak_days": longest_streak_days, "longest_gap": longest_gap, "longest_gap_days": longest_gap_days, "weekend_percent": round(100 * weekend_percent), "best_day_count": best_day_count, "best_day_date": best_day_date, "best_day_index": best_day_index, } ) def format_loc_number(number: int) -> str: if number < 1e3: return str(100 * round(number / 100)) if number < 1e6: return f"{str(round(number / 1000.0))},000" return f"{str(round(number / 1000000.0))},000,000" def get_loc_stats(data: UserPackage) -> LOCStats: dataset = data.contribs.total_stats.languages.values() return LOCStats.model_validate( { "loc_additions": format_loc_number(sum(x.additions for x in dataset)), "loc_deletions": format_loc_number(sum(x.deletions for x in dataset)), "loc_changed": format_loc_number( sum(x.additions + x.deletions for x in dataset) ), "loc_added": format_loc_number( sum(x.additions - x.deletions for x in dataset) ), "loc_additions_per_commit": round( ( sum(x.additions for x in dataset) / max(1, data.contribs.total_stats.commits_count) ) ), "loc_deletions_per_commit": round( ( sum(x.deletions for x in dataset) / max(1, data.contribs.total_stats.commits_count) ) ), "loc_changed_per_day": round( sum(x.additions + x.deletions for x in dataset) / 365 ), } ) def get_numeric_data(data: UserPackage, year: int) -> NumericData: return NumericData.model_validate( { "contribs": get_contrib_stats(data), "misc": get_misc_stats(data, year), "loc": get_loc_stats(data), } ) ================================================ FILE: backend/src/processing/wrapped/package.py ================================================ from src.models import UserPackage, WrappedPackage from src.processing.wrapped.calendar import get_calendar_data from src.processing.wrapped.langs import get_lang_data from src.processing.wrapped.numeric import get_numeric_data from src.processing.wrapped.repos import get_repo_data from src.processing.wrapped.time import get_day_data, get_month_data from src.processing.wrapped.timestamps import get_timestamp_data # from src.processing.user.follows import get_user_follows def get_wrapped_data(user_package: UserPackage, year: int) -> WrappedPackage: """packages all processing steps for the user query""" month_data = get_month_data(user_package) day_data = get_day_data(user_package) calendar_data = get_calendar_data(user_package, year) numeric_data = get_numeric_data(user_package, year) repo_data = get_repo_data(user_package) lang_data = get_lang_data(user_package) timestamp_data = get_timestamp_data(user_package) return WrappedPackage( month_data=month_data, day_data=day_data, calendar_data=calendar_data, numeric_data=numeric_data, repo_data=repo_data, lang_data=lang_data, timestamp_data=timestamp_data, incomplete=user_package.incomplete, ) ================================================ FILE: backend/src/processing/wrapped/repos.py ================================================ from typing import List from src.models import Language, RepoContributionStats, RepoData, RepoDatum, UserPackage from src.utils import format_number def _count_loc(x: Language, metric: str) -> int: if metric == "changed": return x.additions + x.deletions return x.additions - x.deletions def _count_repo_loc(x: RepoContributionStats, metric: str) -> int: return sum(_count_loc(lang, metric) for lang in x.languages.values()) def get_repo_data(data: UserPackage) -> RepoData: out = {} for m in ["changed", "added"]: repos = sorted( data.contribs.repo_stats.items(), key=lambda x: _count_repo_loc(x[1], m), reverse=True, ) repo_objs: List[RepoDatum] = [] # first five repositories for i, (k, v) in enumerate(list(repos)[:5]): repo_data = { "id": i, "label": "private/repository" if v.private else k, "value": _count_repo_loc(v, m), "formatted_value": format_number(_count_repo_loc(v, m)), } repo_objs.append(RepoDatum.model_validate(repo_data)) # remaining repositories total_count = sum(_count_repo_loc(v, m) for _, v in list(repos)[5:]) repo_data = { "id": -1, "label": "other", "value": total_count, "formatted_value": format_number(total_count), } if total_count > 100: repo_objs.append(RepoDatum.model_validate(repo_data)) out[f"repos_{m}"] = repo_objs return RepoData.model_validate(out) ================================================ FILE: backend/src/processing/wrapped/time.py ================================================ from collections import defaultdict from datetime import datetime from typing import Dict, List, Union from src.models import DayData, MonthData, TimeDatum, UserPackage from src.utils import format_number def get_month_data(data: UserPackage) -> MonthData: months: Dict[int, Dict[str, int]] = defaultdict( lambda: {"contribs": 0, "loc_changed": 0} ) for item in data.contribs.total: month = datetime.fromisoformat(item.date).month - 1 months[month]["contribs"] += item.stats.contribs_count loc_changed = sum( x.additions + x.deletions for x in item.stats.languages.values() ) months[month]["loc_changed"] += loc_changed out: List[TimeDatum] = [] for k in range(12): v = months[k] _obj: Dict[str, Union[str, int]] = { "index": k, **v, "formatted_loc_changed": format_number(v["loc_changed"]), } out.append(TimeDatum.model_validate(_obj)) return MonthData(months=out) def get_day_data(data: UserPackage) -> DayData: days: Dict[int, Dict[str, int]] = defaultdict( lambda: {"contribs": 0, "loc_changed": 0} ) for item in data.contribs.total: day = (datetime.fromisoformat(item.date).weekday() + 1) % 7 days[day]["contribs"] += item.stats.contribs_count loc_changed = sum( x.additions + x.deletions for x in item.stats.languages.values() ) days[day]["loc_changed"] += loc_changed out: List[TimeDatum] = [] for k in range(7): v = days[k] _obj: Dict[str, Union[str, int]] = { "index": k, **v, "formatted_loc_changed": format_number(v["loc_changed"]), } out.append(TimeDatum.model_validate(_obj)) return DayData(days=out) ================================================ FILE: backend/src/processing/wrapped/timestamps.py ================================================ from datetime import datetime from random import shuffle from typing import Any, List from src.models import TimestampData, TimestampDatum, UserPackage MAX_ITEMS = 200 def date_to_seconds_since_midnight(date: datetime) -> int: return (date.hour * 60 * 60) + (date.minute * 60) + date.second def get_timestamp_data(data: UserPackage) -> TimestampData: out: List[Any] = [] for item in data.contribs.total: lists = item.lists lists = [lists.commits, lists.issues, lists.prs, lists.reviews] for type, list in zip(["commit", "issue", "pr", "review"], lists): out.extend( { "type": type, "weekday": item.weekday, "timestamp": date_to_seconds_since_midnight(obj), } for obj in list ) shuffle(out) out = out[:MAX_ITEMS] out = [TimestampDatum.model_validate(x) for x in out] return TimestampData(contribs=out) ================================================ FILE: backend/src/render/__init__.py ================================================ from src.render.error import ( get_empty_demo_svg, get_error_svg, get_loading_svg, get_no_data_svg, ) from src.render.top_langs import get_top_langs_svg from src.render.top_repos import get_top_repos_svg __all__ = [ "get_empty_demo_svg", "get_error_svg", "get_loading_svg", "get_no_data_svg", "get_top_langs_svg", "get_top_repos_svg", ] ================================================ FILE: backend/src/render/error.py ================================================ # type: ignore from svgwrite import Drawing from src.constants import BACKEND_URL from src.render.style import styles_no_animation, themes from src.render.template import get_template THEME = "classic" def get_error_svg() -> Drawing: d = Drawing(size=(300, 285)) d.defs.add(d.style(styles_no_animation[THEME])) d.add( d.rect( size=(299, 284), insert=(0.5, 0.5), rx=4.5, stroke=themes[THEME]["border_color"], fill=themes[THEME]["bg_color"], ) ) d.add(d.text("Unknown Error", insert=(25, 35), class_=f"{THEME}-header")) d.add( d.text( "Please try again later or raise a ticket on GitHub", insert=(25, 60), class_=f"{THEME}-lang-name", ) ) d.add( d.image(f"{BACKEND_URL}/assets/error", insert=(85, 100), style="opacity: 50%") ) return d def get_empty_demo_svg(header: str) -> Drawing: d = Drawing(size=(300, 285)) d.defs.add(d.style(styles_no_animation[THEME])) d.add( d.rect( size=(299, 284), insert=(0.5, 0.5), rx=4.5, stroke=themes[THEME]["border_color"], fill=themes[THEME]["bg_color"], ) ) d.add(d.text(header, insert=(25, 35), class_=f"{THEME}-header")) d.add( d.text( "Enter your username to start!", insert=(25, 60), class_=f"{THEME}-lang-name", ) ) d.add( d.image( f"{BACKEND_URL}/assets/stopwatch", insert=(85, 100), style="opacity: 50%" ) ) return d def get_loading_svg() -> Drawing: d = Drawing(size=(300, 285)) d.defs.add(d.style(styles_no_animation[THEME])) d.add( d.rect( size=(299, 284), insert=(0.5, 0.5), rx=4.5, stroke=themes[THEME]["border_color"], fill=themes[THEME]["bg_color"], ) ) d.add( d.text("Loading data, hang tight!", insert=(25, 35), class_=f"{THEME}-header") ) d.add( d.text( "Please wait a couple seconds and refresh the page.", insert=(25, 60), class_=f"{THEME}-lang-name", ) ) d.add( d.image( f"{BACKEND_URL}/assets/stopwatch", insert=(85, 100), style="opacity: 50%" ) ) return d def get_no_data_svg(header: str, subheader: str) -> Drawing: d, dp = get_template( width=300, height=285, padding=20, header_text=header, subheader_text=subheader, debug=False, theme=THEME, ) d.add(d.image(f"{BACKEND_URL}/assets/error", insert=(85, 80), style="opacity: 50%")) dp.add(d.text("No data to show", insert=(55, 220), class_=f"{THEME}-image-text")) d.add(dp) return d ================================================ FILE: backend/src/render/style.py ================================================ from typing import List, Tuple themes = { "classic": { "header_color": "#2f80ed", "subheader_color": "#666", "text_color": "#333", "bg_color": "#fffefe", "border_color": "#e4e2e2", "bar_color": "#ddd", }, "dark": { "header_color": "#fff", "subheader_color": "#9f9f9f", "text_color": "#9f9f9f", "bg_color": "#151515", "border_color": "#e4e2e2", "bar_color": "#333", }, "bright_lights": { "header_color": "#fff", "subheader_color": "#0e86d4", "text_color": "#b1d4e0", "bg_color": "#003060", "border_color": "#0e86d4", "bar_color": "#ddd", }, "rosettes": { "header_color": "#fff", "subheader_color": "#b6e2d3", "text_color": "#fae8e0", "bg_color": "#ef7c8e", "border_color": "#b6e2d3", "bar_color": "#ddd", }, "ferns": { "header_color": "#116530", "subheader_color": "#18a558", "text_color": "#116530", "bg_color": "#a3ebb1", "border_color": "#21b6a8", "bar_color": "#ddd", }, "synthwaves": { "header_color": "#e2e9ec", "subheader_color": "#e5289e", "text_color": "#ef8539", "bg_color": "#2b213a", "border_color": "#e5289e", "bar_color": "#ddd", }, } def get_style(theme: str = "classic", use_animation: bool = True) -> str: # List[Tuple["selector", List[Tuple["property", "is_animation"]], "is_animation"]] _style: List[Tuple[str, List[Tuple[str, bool]], bool]] = [ ( ".header", [ ("font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;", False), ("fill: " + themes[theme]["header_color"] + ";", False), ("animation: fadeInAnimation 0.8s ease-in-out forwards;", True), ], False, ), ( ".subheader", [ ("font: 500 10px 'Segoe UI', Ubuntu, Sans-Serif;", False), ("fill: " + themes[theme]["subheader_color"] + ";", False), ("animation: fadeInAnimation 0.8s ease-in-out forwards;", True), ], False, ), ( ".lang-name", [ ("font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif;", False), ("fill: " + themes[theme]["text_color"] + ";", False), ], False, ), ( ".image-text", [ ("font: 500 20px 'Segoe UI', Ubuntu, Sans-Serif;", False), ("fill: " + themes[theme]["text_color"] + ";", False), ("opacity: 50%;", False), ], False, ), ( "@keyframes fadeInAnimation", [("from { opacity: 0; } to { opacity: 1; }", True)], True, ), ] return "\n".join( [ ( (rule[0].replace(".", f".{theme}-") + " {") + "\n".join( item[0] for item in rule[1] if (use_animation or not item[1]) ) + "}" ) for rule in _style if use_animation or not rule[2] ] ) styles = {k: get_style(k) for k in themes.keys()} styles_no_animation = {k: get_style(k, False) for k in themes.keys()} ================================================ FILE: backend/src/render/template.py ================================================ # type: ignore from typing import List, Tuple from svgwrite import Drawing from svgwrite.container import Group from svgwrite.shapes import Circle from src.constants import DEFAULT_COLOR from src.render.style import styles, styles_no_animation, themes def get_template( width: int, height: int, padding: int, header_text: str, subheader_text: str, theme: str, use_animation: bool = True, debug: bool = False, ) -> Tuple[Drawing, Group]: d = Drawing(size=(width, height)) style = styles[theme] style_no_animation = styles_no_animation[theme] d.defs.add(d.style(style if use_animation else style_no_animation)) d.add( d.rect( size=(width - 1, height - 1), insert=(0.5, 0.5), rx=4.5, stroke=themes[theme]["border_color"], fill=themes[theme]["bg_color"], ) ) d.add( d.rect( size=(width - 2 * padding, height - 2 * padding), insert=(padding, padding), fill="#eee" if debug else themes[theme]["bg_color"], ) ) dp = Group(transform=f"translate({padding}, {padding})") dp.add(d.text(header_text, insert=(0, 13), class_=f"{theme}-header")) dp.add(d.text(subheader_text, insert=(0, 31), class_=f"{theme}-subheader")) return d, dp def get_bar_section( d: Drawing, dataset: List[Tuple[str, str, List[Tuple[float, str]]]], theme: str, padding: int = 45, bar_width: int = 210, ) -> Group: section = Group(transform=f"translate(0, {padding})") for i, (top_text, right_text, data_row) in enumerate(dataset): translate = f"translate(0, {str(40 * i)})" row = Group(transform=translate) row.add(d.text(top_text, insert=(2, 15), class_=f"{theme}-lang-name")) row.add( d.text(right_text, insert=(bar_width + 10, 33), class_=f"{theme}-lang-name") ) progress = Drawing(width=str(bar_width), x="0", y="25") progress.add( d.rect( size=(bar_width, 8), insert=(0, 0), rx=5, ry=5, fill=themes[theme]["bar_color"], ) ) total_percent, total_items = 0, len(data_row) diff = max(0, 300 / bar_width - data_row[-1][0]) for j, (percent, color) in enumerate(data_row): color = color or DEFAULT_COLOR if j == 0: percent -= diff elif j == total_items - 1: percent += diff bar_percent = bar_width * percent / 100 bar_total = bar_width * total_percent / 100 box_size, insert = (bar_percent, 8), (bar_total, 0) progress.add(d.rect(size=box_size, insert=insert, rx=5, ry=5, fill=color)) width = min(bar_percent / 2, 5) if total_items > 1: box_left, box_right = j > 0, j < total_items - 1 box_size, insert = bar_percent - 2 * width, bar_total + width if box_left: box_size += width insert -= width if box_right: box_size += width progress.add(d.rect(size=(box_size, 8), insert=(insert, 0), fill=color)) total_percent += percent row.add(progress) section.add(row) return section def get_lang_name_section( d: Drawing, data: List[Tuple[str, str]], theme: str, columns: int = 2, padding: int = 80, ) -> Group: section = Group(transform=f"translate(0, {padding})") for i, x in enumerate(data): x_translate = str((260 / columns) * (i % columns)) y_translate = str(20 * (i // columns)) lang = Group(transform=f"translate({x_translate}, {y_translate})") lang.add(Circle(center=(5, 5), r=5, fill=(data[i][1] or DEFAULT_COLOR))) lang.add(d.text(data[i][0], insert=(14, 9), class_=f"{theme}-lang-name")) section.add(lang) return section ================================================ FILE: backend/src/render/top_langs.py ================================================ # type: ignore from typing import List, Tuple from svgwrite import Drawing from src.models.svg import LanguageStats from src.render.error import get_no_data_svg from src.render.template import get_bar_section, get_lang_name_section, get_template from src.utils import format_number def get_top_langs_svg( data: List[LanguageStats], time_str: str, use_percent: bool, loc_metric: str, complete: bool, commits_excluded: int, compact: bool, use_animation: bool, theme: str, ) -> Drawing: header = "Most Used Languages" subheader = time_str if not use_percent: subheader += " | " + ("LOC Changed" if loc_metric == "changed" else "LOC Added") if not complete: subheader += " | Incomplete (refresh to update)" elif commits_excluded > 50: subheader += f" | {commits_excluded} commits excluded" if len(data) <= 1: return get_no_data_svg(header, subheader) d, dp = get_template( width=300, height=175 if compact else 285, padding=20, header_text=header, subheader_text=subheader, use_animation=use_animation, debug=False, theme=theme, ) dataset: List[Tuple[str, str, List[Tuple[float, str]]]] = [] padding, width = 0, 0 if compact: data_row = [(x.percent, x.color) for x in data[1:6]] dataset.append(("", "", data_row)) padding, width = 30, 260 else: max_length = max(data[i].loc for i in range(1, len(data))) for x in data[1:6]: if use_percent: dataset.append((x.lang, f"{str(x.percent)}%", [(x.percent, x.color)])) else: percent = 100 * x.loc / max_length dataset.append((x.lang, format_number(x.loc), [(percent, x.color)])) padding, width = 45, 210 if use_percent else 195 dp.add( get_bar_section( d=d, dataset=dataset, theme=theme, padding=padding, bar_width=width ) ) langs = [(f"{x.lang} {str(x.percent)}%", x.color) for x in data[1:6]] if compact: dp.add(get_lang_name_section(d=d, data=langs, theme=theme)) d.add(dp) return d ================================================ FILE: backend/src/render/top_repos.py ================================================ # type: ignore from collections import defaultdict from typing import List, Tuple from svgwrite import Drawing from src.models.svg import RepoStats from src.render.error import get_no_data_svg from src.render.template import get_bar_section, get_lang_name_section, get_template from src.utils import format_number def get_top_repos_svg( data: List[RepoStats], time_str: str, loc_metric: str, complete: bool, commits_excluded: int, use_animation: bool, theme: str, ) -> Drawing: header = "Most Contributed Repositories" subheader = time_str subheader += " | " + ("LOC Changed" if loc_metric == "changed" else "LOC Added") if not complete: subheader += " | Incomplete (refresh to update)" elif commits_excluded > 50: subheader += f" | {commits_excluded} commits excluded" if len(data) == 0: return get_no_data_svg(header, subheader) d, dp = get_template( width=300, height=285, padding=20, header_text=header, subheader_text=subheader, use_animation=use_animation, debug=False, theme=theme, ) dataset: List[Tuple[str, str, List[Tuple[float, str]]]] = [] total = max(x.loc for x in data) for x in data[:4]: data_row = [ (100 * lang.loc / total, lang.color) for lang in sorted(x.langs, key=lambda x: x.loc, reverse=True) ] name = "private/repository" if x.private else x.repo dataset.append((name, format_number(x.loc), data_row)) dp.add( get_bar_section(d=d, dataset=dataset, theme=theme, padding=45, bar_width=195) ) langs = defaultdict(int) for x in data[:4]: for lang in x.langs: langs[(lang.lang, lang.color)] += lang.loc langs = sorted(langs.items(), key=lambda x: x[1], reverse=True) langs = [lang[0] for lang in langs[:6]] columns = {1: 1, 2: 2, 3: 3, 4: 2, 5: 3, 6: 3}[len(langs)] padding = 215 + (10 if columns == len(langs) else 0) dp.add( get_lang_name_section( d=d, data=langs, theme=theme, columns=columns, padding=padding ) ) d.add(dp) return d ================================================ FILE: backend/src/routers/__init__.py ================================================ from src.routers.assets.assets import router as asset_router from src.routers.auth.main import router as auth_router from src.routers.dev import router as dev_router from src.routers.users.main import router as user_router from src.routers.wrapped import router as wrapped_router __all__ = ["asset_router", "auth_router", "dev_router", "user_router", "wrapped_router"] ================================================ FILE: backend/src/routers/assets/__init__.py ================================================ ================================================ FILE: backend/src/routers/assets/assets.py ================================================ from fastapi import APIRouter, status from fastapi.responses import FileResponse router = APIRouter() @router.get("/error", status_code=status.HTTP_200_OK, include_in_schema=False) async def get_error_img(): return FileResponse("./src/routers/assets/assets/error.png") @router.get("/stopwatch", status_code=status.HTTP_200_OK, include_in_schema=False) async def get_stopwatch_img(): return FileResponse("./src/routers/assets/assets/stopwatch.png") ================================================ FILE: backend/src/routers/auth/__init__.py ================================================ ================================================ FILE: backend/src/routers/auth/main.py ================================================ from fastapi import APIRouter from src.routers.auth.standalone import router as standalone_router from src.routers.auth.website import router as website_router router = APIRouter() router.include_router(standalone_router, prefix="") router.include_router(website_router, prefix="/web") ================================================ FILE: backend/src/routers/auth/standalone.py ================================================ import logging from typing import Optional from fastapi import APIRouter from fastapi.responses import RedirectResponse from src.constants import OAUTH_CLIENT_ID from src.processing.auth import authenticate, delete_user from src.routers.decorators import get_redirect_url router = APIRouter() @router.get("/signup/public") def redirect_public(user_id: Optional[str] = None) -> RedirectResponse: return RedirectResponse(get_redirect_url(private=False, user_id=user_id)) @router.get("/signup/private") def redirect_private(user_id: Optional[str] = None) -> RedirectResponse: return RedirectResponse(get_redirect_url(private=True, user_id=user_id)) @router.get("/redirect", include_in_schema=False) async def redirect_return(code: str = "", private_access: bool = False) -> str: try: user_id = await authenticate(code=code, private_access=private_access) return f"You ({user_id}) are now authenticated!" except Exception as e: logging.exception(e) return "Unknown Error. Please try again later." @router.get("/delete/{user_id}") async def delete_account_auth(user_id: str) -> RedirectResponse: return RedirectResponse( get_redirect_url(prefix=f"delete/{user_id}", private=False, user_id=user_id) ) @router.get("/redirect/delete/{user_id}", include_in_schema=False) async def delete_account(user_id: str) -> RedirectResponse: await delete_user(user_id, user_key="", use_user_key=False) return RedirectResponse( f"https://github.com/settings/connections/applications/{OAUTH_CLIENT_ID}" ) ================================================ FILE: backend/src/routers/auth/website.py ================================================ from typing import Any, Dict from fastapi import BackgroundTasks, status from fastapi.responses import Response from fastapi.routing import APIRouter from src.processing.auth import authenticate, delete_user, set_user_key from src.routers.background import run_in_background from src.utils import async_fail_gracefully router = APIRouter() @router.post( "/set_user_key/{code}/{user_key}", status_code=status.HTTP_200_OK, include_in_schema=False, response_model=Dict[str, Any], ) @async_fail_gracefully async def set_user_key_endpoint(response: Response, code: str, user_key: str) -> str: return await set_user_key(code, user_key) @router.post( "/login/{code}", status_code=status.HTTP_200_OK, include_in_schema=False, response_model=Dict[str, Any], ) @async_fail_gracefully async def authenticate_endpoint( response: Response, background_tasks: BackgroundTasks, code: str, private_access: bool = False, ) -> str: output, background_task = await authenticate(code, private_access) if background_task is not None: # set a background task to update the user background_tasks.add_task(run_in_background, task=background_task) return output @router.get( "/delete/{user_id}", status_code=status.HTTP_200_OK, include_in_schema=False, response_model=Dict[str, Any], ) @async_fail_gracefully async def delete_user_endpoint(response: Response, user_id: str, user_key: str) -> bool: return await delete_user(user_id, user_key=user_key) ================================================ FILE: backend/src/routers/background.py ================================================ from typing import Dict from src.aggregation.layer1 import query_user from src.models.background import UpdateUserBackgroundTask # create a cache for the function cache: Dict[str, Dict[str, bool]] = {"update_user": {}} async def run_in_background(task: UpdateUserBackgroundTask): if isinstance(task, UpdateUserBackgroundTask): # type: ignore inputs = { "user_id": task.user_id, "access_token": task.access_token, "private_access": task.private_access, "start_date": task.start_date, "end_date": task.end_date, } inputs = {k: v for k, v in inputs.items() if v is not None} # check if the task is already running if task.user_id in cache["update_user"]: return # add the task to the cache cache["update_user"][task.user_id] = True await query_user(**inputs) # type: ignore # remove the task from the cache del cache["update_user"][task.user_id] ================================================ FILE: backend/src/routers/decorators.py ================================================ import io import logging from datetime import datetime from functools import wraps from typing import Any, Callable, Dict, List, Optional from fastapi import Response, status from starlette.responses import RedirectResponse from svgwrite.drawing import Drawing # type: ignore from src.constants import OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI from src.render import get_error_svg # for standalone auth routes def get_redirect_url( prefix: str = "", private: bool = False, user_id: Optional[str] = None ) -> str: url = ( "https://github.com/login/oauth/authorize?client_id=" + OAUTH_CLIENT_ID + "&redirect_uri=" + OAUTH_REDIRECT_URI + "/redirect" ) # add prefix to redirect to different backend routes if prefix != "": url += f"/{prefix}" # add private flag to request correct permissions if private: url += "?private_access=True&scope=user,repo" else: url += "?private_access=False" # add user_id to hint if provided if user_id is not None: url += f"&login={user_id}" return url # NOTE: implied async, sync not implemented yet def svg_fail_gracefully(func: Callable[..., Any]): @wraps(func) # needed to play nice with FastAPI decorator async def wrapper( response: Response, *args: List[Any], **kwargs: Dict[str, Any] ) -> Any: d: Drawing start = datetime.now() cache_max_age = 3600 try: d = await func(response, *args, **kwargs) except LookupError as e: if "user_id" in kwargs: user_id: str = kwargs["user_id"] # type: ignore url = get_redirect_url(private=False, user_id=user_id) return RedirectResponse(url) logging.exception(e) d = get_error_svg() cache_max_age = 0 except Exception as e: logging.exception(e) d = get_error_svg() cache_max_age = 0 sio = io.StringIO() d.write(sio) # type: ignore print("SVG", datetime.now() - start) return Response( sio.getvalue(), media_type="image/svg+xml", status_code=status.HTTP_200_OK, headers={"Cache-Control": f"public, max-age={cache_max_age}"}, ) return wrapper ================================================ FILE: backend/src/routers/dev.py ================================================ from datetime import date, timedelta from typing import Any, Dict, Optional from fastapi import APIRouter, Response, status from src.aggregation.layer0 import get_user_data from src.data.mongo.secret import update_keys from src.models import UserPackage, WrappedPackage from src.processing.wrapped.package import get_wrapped_data from src.utils import async_fail_gracefully, use_time_range router = APIRouter() @router.get( "/user/{user_id}", status_code=status.HTTP_200_OK, response_model=Dict[str, Any] ) @async_fail_gracefully async def get_user_raw( response: Response, user_id: str, access_token: Optional[str] = None, start_date: date = date.today() - timedelta(365), end_date: date = date.today(), time_range: str = "one_month", timezone_str: str = "US/Eastern", full: bool = False, ) -> UserPackage: await update_keys() start_date, end_date, _ = use_time_range(time_range, start_date, end_date) return await get_user_data( user_id, start_date, end_date, timezone_str, access_token ) @router.get( "/wrapped/{user_id}", status_code=status.HTTP_200_OK, response_model=Dict[str, Any] ) @async_fail_gracefully async def get_wrapped_user_raw( response: Response, user_id: str, year: int = 2024, access_token: Optional[str] = None, ) -> WrappedPackage: await update_keys() user_data = await get_user_data( user_id, date(year, 1, 1), date(year, 12, 31), "US/Eastern", access_token ) return get_wrapped_data(user_data, year) ================================================ FILE: backend/src/routers/users/__init__.py ================================================ ================================================ FILE: backend/src/routers/users/db.py ================================================ from typing import Any, Dict, Optional from fastapi import APIRouter, Response, status from src.data.mongo.secret import update_keys from src.data.mongo.user import PublicUserModel, get_public_user as db_get_public_user from src.utils import async_fail_gracefully router = APIRouter() @router.get( "/update_keys", status_code=status.HTTP_200_OK, include_in_schema=False, response_model=Dict[str, Any], ) @async_fail_gracefully async def update_keys_endpoint(response: Response) -> bool: await update_keys(no_cache=True) return True @router.get( "/get/metadata/{user_id}", status_code=status.HTTP_200_OK, include_in_schema=False, response_model=Dict[str, Any], ) @async_fail_gracefully async def get_db_public_user( response: Response, user_id: str, no_cache: bool = False ) -> Optional[PublicUserModel]: return await db_get_public_user(user_id, no_cache=no_cache) """ @router.get("/get/{user_id}", status_code=status.HTTP_200_OK, include_in_schema=False) @async_fail_gracefully async def get_db_user( response: Response, user_id: str, no_cache: bool = False ) -> Optional[ExternalUserModel]: user: Optional[UserModel] = await get_user_by_user_id(user_id, no_cache=no_cache) if user is None: return None return ExternalUserModel.parse_obj(user.dict()) """ ================================================ FILE: backend/src/routers/users/main.py ================================================ from datetime import date, timedelta from typing import Any, Dict, Optional from fastapi import APIRouter, BackgroundTasks, Response, status from src.aggregation.layer2 import get_user from src.models import UserPackage from src.routers.background import run_in_background from src.routers.users.db import router as db_router from src.routers.users.svg import router as svg_router from src.utils import async_fail_gracefully router = APIRouter() router.include_router(db_router, prefix="/db") router.include_router(svg_router, prefix="/svg") """ ANALYTICS """ @router.get("/{user_id}", status_code=status.HTTP_200_OK, response_model=Dict[str, Any]) @async_fail_gracefully async def get_user_endpoint( response: Response, background_tasks: BackgroundTasks, user_id: str, start_date: date = date.today() - timedelta(365), end_date: date = date.today(), timezone_str: str = "US/Eastern", no_cache: bool = False, ) -> Optional[UserPackage]: output, _, background_task = await get_user( user_id, start_date, end_date, no_cache=no_cache ) if background_task is not None: # set a background task to update the user background_tasks.add_task(run_in_background, task=background_task) return output ================================================ FILE: backend/src/routers/users/svg.py ================================================ from datetime import date, timedelta from typing import Any from fastapi import BackgroundTasks, Response, status from fastapi.responses import HTMLResponse from fastapi.routing import APIRouter from src.processing.user import get_top_languages, get_top_repos, svg_base from src.render import ( get_empty_demo_svg, get_loading_svg, get_top_langs_svg, get_top_repos_svg, ) from src.routers.background import run_in_background from src.routers.decorators import svg_fail_gracefully router = APIRouter() @router.get( "/{user_id}/langs", status_code=status.HTTP_200_OK, response_class=HTMLResponse ) @svg_fail_gracefully async def get_user_lang_svg( response: Response, background_tasks: BackgroundTasks, user_id: str, start_date: date = date.today() - timedelta(30), end_date: date = date.today(), time_range: str = "one_year", timezone_str: str = "US/Eastern", use_percent: bool = False, include_private: bool = False, loc_metric: str = "added", compact: bool = False, demo: bool = False, no_cache: bool = False, use_animation: bool = True, theme: str = "classic", ) -> Any: output, complete, background_task, time_str = await svg_base( user_id, start_date, end_date, time_range, demo, no_cache ) if background_task is not None: # set a background task to update the user background_tasks.add_task(run_in_background, task=background_task) # if no data, return loading svg if output is None: return get_loading_svg() # get top languages processed, commits_excluded = get_top_languages(output, loc_metric, include_private) return get_top_langs_svg( data=processed, time_str=time_str, use_percent=use_percent, loc_metric=loc_metric, complete=complete, commits_excluded=commits_excluded, compact=compact, use_animation=use_animation, theme=theme, ) @router.get( "/{user_id}/repos", status_code=status.HTTP_200_OK, response_class=HTMLResponse ) @svg_fail_gracefully async def get_user_repo_svg( response: Response, background_tasks: BackgroundTasks, user_id: str, start_date: date = date.today() - timedelta(30), end_date: date = date.today(), time_range: str = "one_year", timezone_str: str = "US/Eastern", include_private: bool = False, group: str = "none", loc_metric: str = "added", demo: bool = False, no_cache: bool = False, use_animation: bool = True, theme: str = "classic", ) -> Any: output, complete, background_task, time_str = await svg_base( user_id, start_date, end_date, time_range, demo, no_cache ) if background_task is not None: # set a background task to update the user background_tasks.add_task(run_in_background, task=background_task) # if no data, return loading svg if output is None: return get_loading_svg() # get top repos processed, commits_excluded = get_top_repos( output, loc_metric, include_private, group ) return get_top_repos_svg( data=processed, time_str=time_str, loc_metric=loc_metric, complete=complete, commits_excluded=commits_excluded, use_animation=use_animation, theme=theme, ) @router.get( "/demo", status_code=status.HTTP_200_OK, response_class=HTMLResponse, include_in_schema=False, ) @svg_fail_gracefully async def get_demo_svg(response: Response, card: str) -> Any: if card == "langs": return get_empty_demo_svg("Most Used Languages") elif card == "repos": return get_empty_demo_svg("Most Contributed Repositories") else: return get_empty_demo_svg(card) ================================================ FILE: backend/src/routers/wrapped.py ================================================ from typing import Any, Dict, Optional from fastapi import APIRouter, Response, status from src.aggregation.layer2 import get_is_valid_user from src.models import WrappedPackage from src.processing.wrapped import query_wrapped_user from src.utils import async_fail_gracefully router = APIRouter() @router.get( "/valid/{user_id}", status_code=status.HTTP_200_OK, response_model=Dict[str, Any] ) @async_fail_gracefully async def check_valid_user(response: Response, user_id: str) -> str: return await get_is_valid_user(user_id) @router.get("/{user_id}", status_code=status.HTTP_200_OK, response_model=Dict[str, Any]) @async_fail_gracefully async def get_wrapped_user( response: Response, user_id: str, year: int = 2024, no_cache: bool = False ) -> Optional[WrappedPackage]: valid_user = await get_is_valid_user(user_id) if "Valid user" not in valid_user: return WrappedPackage.empty() return await query_wrapped_user(user_id, year, no_cache=no_cache) ================================================ FILE: backend/src/utils/__init__.py ================================================ from src.utils.alru_cache import alru_cache from src.utils.decorators import async_fail_gracefully, fail_gracefully from src.utils.gather import gather from src.utils.utils import date_to_datetime, format_number, use_time_range __all__ = [ "alru_cache", "async_fail_gracefully", "fail_gracefully", "gather", "date_to_datetime", "format_number", "use_time_range", ] ================================================ FILE: backend/src/utils/alru_cache.py ================================================ from datetime import datetime, timedelta from functools import wraps from typing import ( Any, Awaitable, Callable, Dict, FrozenSet, List, ParamSpec, Tuple, TypeVar, ) Param = ParamSpec("Param") TOutput = TypeVar("TOutput") TKey = Tuple[Tuple[Any, ...], FrozenSet[Tuple[str, Any]]] def alru_cache(max_size: int = 128, ttl: timedelta = timedelta(minutes=1)): def decorator( func: Callable[Param, Awaitable[Tuple[bool, TOutput]]] ) -> Callable[Param, Awaitable[TOutput]]: cache: Dict[TKey, Tuple[datetime, TOutput]] = {} keys: List[TKey] = [] def in_cache(key: TKey) -> bool: # key not in cache if key not in cache: return False # key in cache but expired if datetime.now() - cache[key][0] > ttl: return False # key in cache and not expired return True def update_cache_and_return(key: TKey, flag: bool, value: TOutput) -> TOutput: # if flag = False, do not update cache and return value if not flag: return value # if flag = True, update cache now = datetime.now() cache[key] = (now, value) keys.append(key) # remove oldest key if cache is full if len(keys) > max_size: try: # Should not raise KeyError, but just in case del cache[keys.pop(0)] except KeyError: # Already deleted by another thread pass # return value from cache return value # equal to cache[key][1] @wraps(func) async def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> TOutput: key: TKey = tuple(args), frozenset( [(k, v) for k, v in kwargs.items() if k not in ["no_cache"]] ) if "no_cache" in kwargs and kwargs["no_cache"]: (flag, value) = await func(*args, **kwargs) return update_cache_and_return(key, flag, value) if in_cache(key): return cache[key][1] (flag, value) = await func(*args, **kwargs) return update_cache_and_return(key, flag, value) return wrapper return decorator ================================================ FILE: backend/src/utils/decorators.py ================================================ import logging from datetime import datetime from functools import wraps from typing import Any, Callable, Dict, List from fastapi import Response, status def fail_gracefully(func: Callable[..., Any]): @wraps(func) # needed to play nice with FastAPI decorator def wrapper(response: Response, *args: List[Any], **kwargs: Dict[str, Any]) -> Any: start = datetime.now() try: data = func(response, *args, **kwargs) return {"data": data, "message": "200 OK", "time": datetime.now() - start} except Exception as e: logging.exception(e) response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return { "data": [], "message": f"Error {str(e)}", "time": datetime.now() - start, } return wrapper def async_fail_gracefully(func: Callable[..., Any]): @wraps(func) # needed to play nice with FastAPI decorator async def wrapper( response: Response, *args: List[Any], **kwargs: Dict[str, Any] ) -> Any: start = datetime.now() try: data = await func(response, *args, **kwargs) return {"data": data, "message": "200 OK", "time": datetime.now() - start} except Exception as e: logging.exception(e) response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return { "data": [], "message": f"Error {str(e)}", "time": datetime.now() - start, } return wrapper ================================================ FILE: backend/src/utils/gather.py ================================================ import asyncio from functools import partial, wraps from typing import Any, Callable, Dict, List def async_function(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) async def run(*args: List[Any], **kwargs: Dict[str, Any]) -> Any: loop = asyncio.get_event_loop() pfunc = partial(func, *args, **kwargs) return await loop.run_in_executor(executor=None, func=pfunc) return run async def gather_with_concurrency( n: int, *tasks: List[Callable[..., Any]] ) -> List[Any]: semaphore = asyncio.Semaphore(n) async def sem_task(task: Callable[..., Any]) -> Any: async with semaphore: return await task # type: ignore return await asyncio.gather(*(sem_task(task) for task in tasks)) # type: ignore async def gather( funcs: List[Callable[..., Any]], args_dicts: List[Dict[str, Any]], max_threads: int = 5, ) -> List[Any]: """runs the given functions asynchronously""" output: List[Any] = list( await gather_with_concurrency( max_threads, *(async_function(func)(**kwargs) for func, kwargs in zip(funcs, args_dicts)) ) ) return output ================================================ FILE: backend/src/utils/utils.py ================================================ from datetime import date, datetime, timedelta from typing import Tuple def date_to_datetime( dt: date, hour: int = 0, minute: int = 0, second: int = 0 ) -> datetime: return datetime(dt.year, dt.month, dt.day, hour, minute, second) # returns start date, end date, string representing time range def use_time_range( time_range: str, start_date: date, end_date: date ) -> Tuple[date, date, str]: duration_options = { "one_month": (30, "Past 1 Month"), "three_months": (90, "Past 3 Months"), "six_months": (180, "Past 6 Months"), "one_year": (365, "Past 1 Year"), "all_time": (365 * 10, "All Time"), } start_str = start_date.strftime("X%m/X%d/%Y").replace("X0", "X").replace("X", "") end_str = end_date.strftime("X%m/X%d/%Y").replace("X0", "X").replace("X", "") if end_date == date.today(): end_str = "Present" time_str = f"{start_str} - {end_str}" if time_range in duration_options: days, time_str = duration_options[time_range] end_date = date.today() start_date = date.today() - timedelta(days) return start_date, end_date, time_str def format_number(num: int) -> str: if num > 10000: return f"~{str(num // 1000)}k lines" elif num > 1000: return f"~{str(num // 100 / 10)}k lines" elif num > 100: return f"~{str(num // 100 * 100)} lines" else: return "<100 lines" ================================================ FILE: backend/tests/__init__.py ================================================ from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv(), verbose=True) ================================================ FILE: backend/tests/aggregation/__init__.py ================================================ ================================================ FILE: backend/tests/aggregation/layer0/__init__.py ================================================ ================================================ FILE: backend/tests/aggregation/layer0/test_contributions.py ================================================ from datetime import date, timedelta from aiounittest.case import AsyncTestCase from src.aggregation.layer0.contributions import get_contributions from src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID from src.models import UserContributions class TestTemplate(AsyncTestCase): async def test_get_contributions(self): response = await get_contributions( user_id=USER_ID, start_date=date.today() - timedelta(days=30), end_date=date.today(), access_token=TOKEN, ) self.assertIsInstance(response, UserContributions) # TODO: Add further validation ================================================ FILE: backend/tests/aggregation/layer0/test_follows.py ================================================ import unittest from src.aggregation.layer0.follows import get_user_follows from src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID from src.models import UserFollows class TestTemplate(unittest.TestCase): def test_get_follows(self): response = get_user_follows(USER_ID, TOKEN) self.assertIsInstance(response, UserFollows) # TODO: Add further validation ================================================ FILE: backend/tests/data/__init__.py ================================================ ================================================ FILE: backend/tests/data/github/__init__.py ================================================ ================================================ FILE: backend/tests/data/github/auth/__init__.py ================================================ ================================================ FILE: backend/tests/data/github/auth/test_main.py ================================================ import unittest from src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID from src.data.github.auth.main import get_unknown_user class TestTemplate(unittest.TestCase): def test_get_unknown_user_valid(self): user_id = get_unknown_user(TOKEN) self.assertEqual(user_id, USER_ID) def test_get_unknown_user_invalid(self): user_id = get_unknown_user("") self.assertEqual(user_id, None) # TODO: test authenticate() ================================================ FILE: backend/tests/data/github/graphql/__init__.py ================================================ ================================================ FILE: backend/tests/data/github/graphql/test_commits.py ================================================ import unittest from src.constants import TEST_NODE_IDS as NODE_IDS, TEST_TOKEN as TOKEN from src.data.github.graphql import RawCommit, get_commits class TestTemplate(unittest.TestCase): def test_get_commits(self): node_ids = get_commits(access_token=TOKEN, node_ids=NODE_IDS, catch_errors=True) # assert returns equal number of commits self.assertIsInstance(node_ids, list) self.assertEqual(len(node_ids), len(NODE_IDS)) self.assertIsInstance(node_ids[0], RawCommit) def test_get_commits_invalid_access_token(self): node_ids = get_commits(access_token="", node_ids=NODE_IDS, catch_errors=True) # assert returns list of Nones self.assertIsInstance(node_ids, list) self.assertEqual(len(node_ids), len(NODE_IDS)) self.assertIsInstance(node_ids[0], type(None)) def test_get_commits_invalid_node_ids(self): node_ids = get_commits( access_token=TOKEN, node_ids=[NODE_IDS[0], "", NODE_IDS[1]], catch_errors=True, ) self.assertIsInstance(node_ids[0], RawCommit) self.assertIsInstance(node_ids[1], type(None)) self.assertIsInstance(node_ids[2], RawCommit) ================================================ FILE: backend/tests/data/github/graphql/test_repo.py ================================================ import unittest from src.constants import ( TEST_REPO as REPO, TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID, ) from src.data.github.graphql import RawRepo, get_repo class TestTemplate(unittest.TestCase): def test_get_repo(self): repo: RawRepo = get_repo( # type: ignore access_token=TOKEN, owner=USER_ID, repo=REPO, catch_errors=True ) # assert returns equal number of commits self.assertIsInstance(repo, RawRepo) self.assertEqual(repo.is_private, False) self.assertGreater(repo.fork_count, 0) self.assertGreater(repo.stargazer_count, 0) def test_get_repo_invalid_access_token(self): repo = get_repo(access_token="", owner=USER_ID, repo=REPO, catch_errors=True) # assert returns None self.assertIsInstance(repo, type(None)) def test_get_commits_invalid_args(self): repo = get_repo( access_token=TOKEN, owner="abc123", repo=REPO, catch_errors=True ) self.assertIsInstance(repo, type(None)) ================================================ FILE: backend/tests/data/github/graphql/test_user_contribs.py ================================================ import unittest from datetime import datetime, timedelta from src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID from src.data.github.graphql import ( RawCalendar, RawEvents, get_user_contribution_calendar, get_user_contribution_events, ) class TestTemplate(unittest.TestCase): def test_get_user_contribution_calendar(self): response = get_user_contribution_calendar( user_id=USER_ID, access_token=TOKEN, start_date=datetime.now() - timedelta(days=30), end_date=datetime.now(), ) self.assertIsInstance(response, RawCalendar) def test_get_user_contribution_events(self): response = get_user_contribution_events( user_id=USER_ID, access_token=TOKEN, start_date=datetime.now() - timedelta(days=30), end_date=datetime.now(), ) self.assertIsInstance(response, RawEvents) ================================================ FILE: backend/tests/data/github/graphql/test_user_follows.py ================================================ import unittest from src.constants import TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID from src.data.github.graphql import RawFollows, get_user_followers, get_user_following class TestTemplate(unittest.TestCase): def test_get_user_followers(self): response = get_user_followers(user_id=USER_ID, access_token=TOKEN) self.assertIsInstance(response, RawFollows) response = get_user_followers(user_id=USER_ID, access_token=TOKEN, first=1) self.assertLessEqual(len(response.nodes), 1) def test_get_user_following(self): response = get_user_following(user_id=USER_ID, access_token=TOKEN) self.assertIsInstance(response, RawFollows) response = get_user_following(user_id=USER_ID, access_token=TOKEN, first=1) self.assertLessEqual(len(response.nodes), 1) ================================================ FILE: backend/tests/data/github/rest/__init__.py ================================================ ================================================ FILE: backend/tests/data/github/rest/test_commit.py ================================================ import unittest from src.constants import ( TEST_REPO as REPO, TEST_SHA as SHA, TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID, ) from src.data.github.rest import RawCommitFile, get_commit_files class TestTemplate(unittest.TestCase): def test_get_commit_files(self): commits = get_commit_files( access_token=TOKEN, owner=USER_ID, repo=REPO, sha=SHA ) self.assertIsInstance(commits, list) self.assertIsInstance(commits[0], RawCommitFile) # type: ignore def test_get_commit_files_invalid_access_token(self): repo = get_commit_files(access_token="", owner=USER_ID, repo=REPO, sha=SHA) self.assertEqual(repo, None) def test_get_commit_files_invalid_args(self): repo = get_commit_files(access_token=TOKEN, owner="abc123", repo=REPO, sha=SHA) self.assertEqual(repo, None) ================================================ FILE: backend/tests/data/github/rest/test_repo.py ================================================ import unittest from src.constants import ( TEST_REPO as REPO, TEST_TOKEN as TOKEN, TEST_USER_ID as USER_ID, ) from src.data.github.rest import RawCommit, get_repo_commits class TestTemplate(unittest.TestCase): def test_get_repo_commits(self): commits = get_repo_commits(access_token=TOKEN, owner=USER_ID, repo=REPO) self.assertIsInstance(commits, list) self.assertIsInstance(commits[0], RawCommit) def test_get_repo_commits_invalid_access_token(self): repo = get_repo_commits(access_token="", owner=USER_ID, repo=REPO) self.assertEqual(repo, []) def test_get_repo_commits_invalid_args(self): repo = get_repo_commits(access_token=TOKEN, owner="abc123", repo=REPO) self.assertEqual(repo, []) ================================================ FILE: backend/tests/utils/__init__.py ================================================ ================================================ FILE: backend/tests/utils/test_alru_cache.py ================================================ from asyncio import sleep from datetime import timedelta from typing import Tuple from aiounittest.case import AsyncTestCase from src.utils import alru_cache class TestTemplate(AsyncTestCase): async def test_basic_alru_cache(self): count = 0 @alru_cache() async def f(x: int) -> Tuple[bool, int]: nonlocal count count += 1 return (True, x) assert count == 0 assert await f(1) == 1 assert count == 1 assert await f(1) == 1 assert count == 1 assert await f(2) == 2 assert count == 2 assert await f(2) == 2 assert count == 2 assert await f(1) == 1 assert count == 2 async def test_alru_cache_with_flag(self): count = 0 @alru_cache() async def f(x: int) -> Tuple[bool, int]: nonlocal count count += 1 return (count % 2 == 0, x) assert count == 0 assert await f(1) == 1 assert count == 1 assert await f(1) == 1 assert count == 2 assert await f(2) == 2 assert count == 3 assert await f(3) == 3 assert count == 4 assert await f(3) == 3 assert count == 4 async def test_alru_cache_with_maxsize(self): count = 0 @alru_cache(max_size=2) async def f(x: int) -> Tuple[bool, int]: nonlocal count count += 1 return (True, x) assert count == 0 assert await f(1) == 1 assert count == 1 assert await f(2) == 2 assert count == 2 assert await f(3) == 3 assert count == 3 assert await f(1) == 1 assert count == 4 async def test_alru_cache_with_ttl(self): count = 0 @alru_cache(ttl=timedelta(milliseconds=1)) async def f(x: int) -> Tuple[bool, int]: nonlocal count count += 1 return (True, x) assert count == 0 assert await f(1) == 1 assert count == 1 assert await f(1) == 1 assert count == 1 await sleep(0.01) assert await f(1) == 1 assert count == 2 async def test_alru_cache_with_no_cache(self): count = 0 @alru_cache() async def f(x: int, no_cache: bool = False) -> Tuple[bool, int]: nonlocal count count += 1 return (True, x) assert count == 0 assert await f(1) == 1 assert count == 1 assert await f(1) == 1 assert count == 1 assert await f(2) == 2 assert count == 2 assert await f(2, no_cache=True) == 2 assert count == 3 assert await f(3) == 3 assert count == 4 assert await f(3, no_cache=False) == 3 assert count == 4 ================================================ FILE: backend/transfer_mongodb.bash ================================================ if [ $# -eq 0 ]; then echo "Usage: $0 " exit 1 fi # Export mongoexport --uri "mongodb+srv://backend.aqlpb.mongodb.net/" --db dev_backend --collection secrets --username root --password "$1" > ./dev_secrets.json mongoexport --uri "mongodb+srv://backend.aqlpb.mongodb.net/" --db dev_backend --collection users --username root --password "$1" > ./dev_users.json mongoexport --uri "mongodb+srv://backend.aqlpb.mongodb.net/" --db dev_backend --collection user_months --username root --password "$1" > ./dev_user_months.json mongoexport --uri "mongodb+srv://backend.aqlpb.mongodb.net/" --db prod_backend --collection secrets --username root --password "$1" > ./prod_secrets.json mongoexport --uri "mongodb+srv://backend.aqlpb.mongodb.net/" --db prod_backend --collection users --username root --password "$1" > ./prod_users.json mongoexport --uri "mongodb+srv://backend.aqlpb.mongodb.net/" --db prod_backend --collection user_months --username root --password "$1" > ./prod_user_months.json # Import mongoimport --uri "mongodb+srv://backend2.e50j8dp.mongodb.net/" --db dev_backend --collection secrets --username root --password "$1" < ./dev_secrets.json mongoimport --uri "mongodb+srv://backend2.e50j8dp.mongodb.net/" --db dev_backend --collection users --username root --password "$1" < ./dev_users.json mongoimport --uri "mongodb+srv://backend2.e50j8dp.mongodb.net/" --db dev_backend --collection user_months --username root --password "$1" < ./dev_user_months.json mongoimport --uri "mongodb+srv://backend2.e50j8dp.mongodb.net/" --db prod_backend --collection secrets --username root --password "$1" < ./prod_secrets.json mongoimport --uri "mongodb+srv://backend2.e50j8dp.mongodb.net/" --db prod_backend --collection users --username root --password "$1" < ./prod_users.json mongoimport --uri "mongodb+srv://backend2.e50j8dp.mongodb.net/" --db prod_backend --collection user_months --username root --password "$1" < ./prod_user_months.json # Remove rm ./dev_secrets.json rm ./dev_users.json rm ./dev_user_months.json rm ./prod_secrets.json rm ./prod_users.json rm ./prod_user_months.json ================================================ FILE: docs/API.md ================================================ # GitHub Trends API GitHub Trends provides two methods to access GitHub Trends data: the Website Workflow at githubtrends.io and the API Workflow described below. ## Available Cards After authenticating with either the public or private workflow (see below), users can create the following cards to display their GitHub Trends data: - **[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. - **[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. # Authentication You 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: - 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. - 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. You will only need to authenticate once with GitHub Trends. Subsequent requests will use your stored access token. For the public workflow, visit ```md https://api.githubtrends.io/auth/signup/public ``` For the private workflow, visit ```md https://api.githubtrends.io/auth/signup/private ``` You will be prompted to allow access, and (hopefully) redirected to a success screen. If 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. # Languages Card See 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. After authentication, visit ```md https://api.githubtrends.io/user/svg/{user_id}/langs ``` ## Customization The following customization options are available: | Option | Description | Default | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | | `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` | | `include_private` | Determines if private contributions are included (requires private workflow). | `false` | | `compact` | Determines if compact layout is used (forces percentages over LOC) | `false` | | `use_percent` | Valid if `compact=false`, determines if line of code (default) or percentages are displayed. | `false` | | `loc_metric` | Options are LOC added (`added`) and LOC changed (`changed`). | `added` | | `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` | Customizations can be appended to the endpoint, separated first with `?` and subsequently with `&`. ## Example Endpoint: `https://api.githubtrends.io/user/svg/avgupta456/langs?time_range=three_months&include_private=true&compact=true` [![GitHub Trends SVG](https://api.githubtrends.io/user/svg/avgupta456/langs?time_range=three_months&include_private=true&compact=true)](https://githubtrends.io) # Repositories Card After authentication, visit ```md https://api.githubtrends.io/user/svg/{user_id}/repos ``` ## Customization The following customization options are available: | Option | Description | Default | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | | `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` | | `include_private` | Determines if private contributions are included (requires private workflow). | `false` | | `group` | Options are `none` (default), `other` (group all other repos together), and `private` (force private repos to be grouped) | `none` | | `use_percent` | Valid if `compact=false`, determines if line of code (default) or percentages are displayed. | `false` | | `loc_metric` | Options are LOC added (`added`) and LOC changed (`changed`). | `added` | | `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` | Customizations can be appended to the endpoint, separated first with `?` and subsequently with `&`. ## Example Endpoint: `https://api.githubtrends.io/user/svg/avgupta456/repos?time_range=one_year&include_private=true&group=private&loc_metric=changed&theme=dark` [![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) ================================================ FILE: docs/CONTRIBUTING.md ================================================ # GitHub Trends If 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! ## Local Development First, 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: With Python3.11, install the dependencies from `backend/requirements.txt` and run `yarn start`. With Node16 and Yarn, install the dependencies from `frontend/package.json` and run on a separate terminal window `yarn start-trends`. ## Testing Create 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. ================================================ FILE: docs/FAQ.md ================================================ # FAQ The FAQ is in progress. Reach out if you have any unanswered questions or concerns. --- **Question**: Does GitHub Trends have access to my private code contributions? **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**. Alternatively, 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. **Question**: How can I display my images side by side? **Answer**: Use HTML (credit: [github-readme-stats](https://github.com/anuraghazra/github-readme-stats#quick-tip-align-the-repo-cards)) ``` ``` **Question**: How can I see my stats without giving GitHub Trends my access token? **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: ```bash python ./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 ``` The script will output the raw and processed JSONs into the output directory specified. **Question**: What if I find a bug, or want to contribute? **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. ================================================ FILE: docs/THEME.md ================================================ The following themes are available for all GitHub Trends cards: | Themes | | | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | [![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) | | [![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) | ================================================ FILE: frontend/.env-template ================================================ REACT_APP_PROD=false REACT_APP_CLIENT_ID=abc123 ================================================ FILE: frontend/.eslintrc.js ================================================ module.exports = { env: { browser: true, es6: true, }, extends: ['airbnb', 'plugin:prettier/recommended'], parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 2020, sourceType: 'module', }, plugins: ['react', 'prettier'], rules: { 'react/jsx-filename-extension': 'off', 'react/forbid-prop-types': 'off', 'react/destructuring-assignment': 'off', 'import/prefer-default-export': 'off', 'react/function-component-definition': 'off', 'react/no-unstable-nested-components': 'off', 'jsx-a11y/control-has-associated-label': 'off', 'no-console': 'off', radix: 'off', 'prettier/prettier': [ 'error', { endOfLine: 'auto', }, ], }, }; ================================================ FILE: frontend/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* # custom public/index.html ================================================ FILE: frontend/.prettierrc.js ================================================ module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 80, tabWidth: 2, }; ================================================ FILE: frontend/.yarnrc ================================================ network-timeout 500000 ================================================ FILE: frontend/README.md ================================================ # Frontend ## Installation ``` yarn install ``` ## Run Locally ``` yarn start-trends yarn start-wrapped ``` ## Build ``` yarn build-trends yarn build-wrapped ``` Then, just commit on the main branch (Vercel takes care of the rest) ## Adding a Secret Update .env, .env-template, and Vercel. ================================================ FILE: frontend/deploy/Dockerfile ================================================ FROM node:16-alpine WORKDIR /frontend ENV PATH /frontend/node_modules/.bin:$PATH COPY ../package.json ../yarn.lock /frontend/ RUN yarn install --network-timeout 100000 COPY ../ /frontend CMD ["yarn", "start"] ================================================ FILE: frontend/package.json ================================================ { "name": "frontend", "version": "0.1.0", "private": true, "dependencies": { "@babel/runtime": "^7.23.2", "@nivo/bar": "^0.83.0", "@nivo/calendar": "^0.83.0", "@nivo/core": "^0.83.0", "@nivo/pie": "^0.83.0", "@nivo/radar": "^0.83.0", "@nivo/swarmplot": "^0.83.0", "@nivo/tooltip": "^0.83.0", "axios": "^1.6.1", "daisyui": "2.31.0", "downloadjs": "^1.4.7", "html-to-image": "^1.11.11", "moment": "^2.29.4", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-loading-skeleton": "^3.3.1", "react-redux": "^8.1.3", "react-router-dom": "^6.18.0", "react-scripts": "^5.0.1", "react-select": "^5.8.0", "react-spinners": "^0.13.8", "react-toastify": "^9.1.3", "react-typist": "^2.0.5", "react-typist-loop": "0.0.5", "redux": "^4.2.1", "save-svg-as-png": "^1.4.17", "uuid": "^9.0.1" }, "devDependencies": { "autoprefixer": "^10.4.16", "eslint": "8.53.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "postcss": "^8.4.31", "prettier": "^3.0.3", "tailwindcss": "^3.3.5" }, "scripts": { "setup-trends": "cp ./public/trends.html ./public/index.html", "setup-wrapped": "cp ./public/wrapped.html ./public/index.html", "start-trends": "yarn setup-trends && REACT_APP_MODE=trends react-scripts start", "start-wrapped": "yarn setup-wrapped && REACT_APP_MODE=wrapped PORT=3001 react-scripts start", "build-trends": "yarn setup-trends && REACT_APP_MODE=trends react-scripts build", "build-wrapped": "yarn setup-wrapped && REACT_APP_MODE=wrapped react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: frontend/public/_redirects ================================================ /* /index.html 200 ================================================ FILE: frontend/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: frontend/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: frontend/public/trends.html ================================================ GitHub Trends
================================================ FILE: frontend/public/wrapped.html ================================================ GitHub Wrapped
================================================ FILE: frontend/src/api/index.js ================================================ import { setUserKey, authenticate, getUserMetadata, deleteAccount, } from './user'; import { getWrapped } from './wrapped'; export { setUserKey, authenticate, getUserMetadata, deleteAccount, getWrapped }; ================================================ FILE: frontend/src/api/user.js ================================================ import axios from 'axios'; import { v4 as uuidv4 } from 'uuid'; import { BACKEND_URL } from '../constants'; const URL_PREFIX = BACKEND_URL; const setUserKey = async (code) => { try { const key = uuidv4(); const fullUrl = `${URL_PREFIX}/auth/web/set_user_key/${code}/${key}`; await axios.post(fullUrl); return key; } catch (error) { console.error(error); return ''; } }; const authenticate = async (code, privateAccess) => { try { const fullUrl = `${URL_PREFIX}/auth/web/login/${code}?private_access=${privateAccess}`; const result = await axios.post(fullUrl); return result.data.data; } catch (error) { console.error(error); return ''; } }; const getUserMetadata = async (userId) => { try { const fullUrl = `${URL_PREFIX}/user/db/get/metadata/${userId}`; const result = await axios.get(fullUrl); return result.data.data; } catch (error) { console.error(error); return ''; } }; const deleteAccount = async (userId, userKey) => { try { const fullUrl = `${URL_PREFIX}/auth/web/delete/${userId}?user_key=${userKey}`; const result = await axios.get(fullUrl); return result.data; // no decorator } catch (error) { console.error(error); return ''; } }; export { setUserKey, authenticate, getUserMetadata, deleteAccount }; ================================================ FILE: frontend/src/api/wrapped.js ================================================ /* eslint-disable no-return-await */ import axios from 'axios'; import { BACKEND_URL } from '../constants'; const URL_PREFIX = `${BACKEND_URL}/wrapped`; const getValidUser = async (userId) => { try { const fullUrl = `${URL_PREFIX}/valid/${userId}`; const result = await axios.get(fullUrl); return result.data.data; } catch (error) { return null; } }; const getWrapped = async (userId, year) => { try { const fullUrl = `${URL_PREFIX}/${userId}?year=${year}`; const result = await axios.get(fullUrl); return result.data.data; } catch (error) { return null; } }; export { getWrapped, getValidUser }; ================================================ FILE: frontend/src/assets/notes.txt ================================================ 2022: Generated laptop mockups using deviceshots.com 2023: Used https://www.anthonyboyd.graphics/mockups/m2-macbook-air-mockup/ and Adobe Express ================================================ FILE: frontend/src/components/Card/Card.js ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { BACKEND_URL } from '../../constants'; import SVG from './SVG'; import { classnames } from '../../utils'; export const Image = ({ imageSrc, compact }) => { const userId = useSelector((state) => state.user.userId); const fullImageSrc = `${BACKEND_URL}/user/svg/${userId}/${imageSrc}`; return (
); }; Image.propTypes = { imageSrc: PropTypes.string.isRequired, compact: PropTypes.bool, }; Image.defaultProps = { compact: false, }; export const Card = ({ title, description, imageSrc, selected, compact }) => { return (

{title}

{description}

); }; Card.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, imageSrc: PropTypes.string.isRequired, selected: PropTypes.bool, compact: PropTypes.bool, }; Card.defaultProps = { selected: false, compact: false, }; ================================================ FILE: frontend/src/components/Card/SVG.js ================================================ /* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/no-danger */ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; const SvgInline = (props) => { const [svg, setSvg] = useState(null); // eslint-disable-next-line no-unused-vars const [loaded, setLoaded] = useState(false); let url = `${props.url.split('?')[0]}?cache=${Date.now()}`; if (props.url.split('?').length > 1) { url += `&${props.url.split('?')[1]}`; } useEffect(() => { setLoaded(false); fetch(url) .then((res) => res.text()) .then(setSvg) .then(() => setLoaded(true)) .catch((e) => console.error(e)); }, [props.url]); if (props.forceLoading || !loaded) { if (props.compact) { return ; } return ; } if (props.compact) { return (
${svg}`, }} /> ); } return (
${svg}`, }} /> ); }; SvgInline.propTypes = { className: PropTypes.any, url: PropTypes.string.isRequired, forceLoading: PropTypes.bool, compact: PropTypes.bool, }; SvgInline.defaultProps = { className: '', forceLoading: false, compact: false, }; export default SvgInline; ================================================ FILE: frontend/src/components/Card/index.js ================================================ import SvgInline from './SVG'; import { Card, Image } from './Card'; export { Card, Image, SvgInline }; ================================================ FILE: frontend/src/components/Generic/Button.js ================================================ /* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; import PropTypes from 'prop-types'; import { classnames } from '../../utils'; const Button = (props) => { return ( ); }; Button.propTypes = { className: PropTypes.string, children: PropTypes.node.isRequired, }; Button.defaultProps = { className: '', }; export default Button; ================================================ FILE: frontend/src/components/Generic/Checkbox.js ================================================ /* eslint-disable jsx-a11y/interactive-supports-focus */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import React from 'react'; import PropTypes from 'prop-types'; const Checkbox = ({ question, variable, setVariable, disabled }) => { return (
setVariable(!variable)} role="button" > setVariable(!variable)} /> {question}
); }; Checkbox.propTypes = { question: PropTypes.string.isRequired, variable: PropTypes.bool.isRequired, setVariable: PropTypes.func.isRequired, disabled: PropTypes.bool, }; Checkbox.defaultProps = { disabled: false, }; export default Checkbox; ================================================ FILE: frontend/src/components/Generic/Input.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { classnames } from '../../utils'; // options is of form [{value: '', label: '', disabled: true/false}] const Input = ({ options, selectedOption, setSelectedOption, disabled, className, }) => { return ( ); }; Input.propTypes = { options: PropTypes.arrayOf( PropTypes.shape({ value: PropTypes.string.isRequired, label: PropTypes.string.isRequired, disabled: PropTypes.bool, }), ).isRequired, selectedOption: PropTypes.shape({ value: PropTypes.string.isRequired, label: PropTypes.string.isRequired, }).isRequired, setSelectedOption: PropTypes.func.isRequired, disabled: PropTypes.bool, className: PropTypes.string, }; Input.defaultProps = { disabled: false, className: '', }; export default Input; ================================================ FILE: frontend/src/components/Generic/index.js ================================================ import Button from './Button'; import Checkbox from './Checkbox'; import Input from './Input'; export { Button, Checkbox, Input }; ================================================ FILE: frontend/src/components/Home/CheckboxSection.js ================================================ /* eslint-disable jsx-a11y/interactive-supports-focus */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import React from 'react'; import PropTypes from 'prop-types'; import Section from './Section'; import { Checkbox } from '../Generic'; const CheckboxSection = ({ title, text, question, variable, setVariable, disabled, }) => { return (

{text}

); }; CheckboxSection.propTypes = { title: PropTypes.string.isRequired, text: PropTypes.string.isRequired, question: PropTypes.string.isRequired, variable: PropTypes.bool.isRequired, setVariable: PropTypes.func.isRequired, disabled: PropTypes.bool, }; CheckboxSection.defaultProps = { disabled: false, }; export default CheckboxSection; ================================================ FILE: frontend/src/components/Home/DateRangeSection.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import Section from './Section'; import { Input } from '../Generic'; const DateRangeSection = ({ selectedTimeRange, setSelectedTimeRange, // eslint-disable-next-line no-unused-vars privateAccess, }) => { const timeRangeOptions = [ { id: 1, label: 'Past 1 Month', disabled: false, value: 'one_month' }, { id: 2, label: 'Past 3 Months', disabled: false, value: 'three_months', }, { id: 2, label: 'Past 6 Months', disabled: false, value: 'six_months' }, { id: 3, label: 'Past 1 Year', disabled: false, value: 'one_year' }, // { id: 4, label: 'All Time', disabled: !privateAccess, value: 'all_time' }, { id: 4, label: 'All Time', disabled: true, value: 'all_time' }, ]; const selectedOption = selectedTimeRange || timeRangeOptions[2]; return (

Select the date range for statistics.

); }; DateRangeSection.propTypes = { selectedTimeRange: PropTypes.object.isRequired, setSelectedTimeRange: PropTypes.func.isRequired, privateAccess: PropTypes.bool.isRequired, }; export default DateRangeSection; ================================================ FILE: frontend/src/components/Home/Progress.js ================================================ /* eslint-disable react/no-array-index-key */ import React from 'react'; import PropTypes from 'prop-types'; import { FaArrowLeft as LeftArrowIcon, FaArrowRight as RightArrowIcon, } from 'react-icons/fa'; import { classnames } from '../../utils'; const ProgressSection = ({ num, item, passed, onClick }) => { return ( ); }; ProgressSection.propTypes = { num: PropTypes.number.isRequired, item: PropTypes.string.isRequired, passed: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, }; const ProgressBar = ({ items, currItem, setCurrItem }) => { const leftDisabled = currItem === 0; const rightDisabled = currItem === items.length - 1; return (
setCurrItem(currItem - 1)} />
{items.map((item, index) => { return ( = index} onClick={() => setCurrItem(index)} /> ); })}
setCurrItem(currItem + 1)} />
); }; ProgressBar.propTypes = { items: PropTypes.array.isRequired, currItem: PropTypes.number.isRequired, setCurrItem: PropTypes.func.isRequired, }; export default ProgressBar; ================================================ FILE: frontend/src/components/Home/Section.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { HiOutlineLightningBolt as LightningIcon } from 'react-icons/hi'; const Section = (props) => { return (

{props.title}

{props.children}
); }; Section.propTypes = { title: PropTypes.string, children: PropTypes.node, }; Section.defaultProps = { title: 'Test', children:

This is a test!

, }; export default Section; ================================================ FILE: frontend/src/components/Home/index.js ================================================ import ProgressBar from './Progress'; import CheckboxSection from './CheckboxSection'; import DateRangeSection from './DateRangeSection'; export { ProgressBar, CheckboxSection, DateRangeSection }; ================================================ FILE: frontend/src/components/Preview/Preview.js ================================================ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { FaArrowRight as ArrowRightIcon, FaArrowLeft as ArrowLeftIcon, } from 'react-icons/fa'; import { classnames } from '../../utils'; const Preview = ({ pages, details, showArrows }) => { const totalPages = pages.length; const [page, setPage] = useState(0); const prevPage = () => { setPage((page - 1 + totalPages) % totalPages); }; const nextPage = () => { setPage((page + 1 + totalPages) % totalPages); }; useEffect(() => { const interval = setInterval(nextPage, 5000); return () => clearInterval(interval); }, [page]); return (

{showArrows && ( )} preview {showArrows && ( )}

{details[page]}

); }; Preview.propTypes = { pages: PropTypes.arrayOf(PropTypes.any).isRequired, details: PropTypes.arrayOf(PropTypes.string).isRequired, showArrows: PropTypes.bool, }; Preview.defaultProps = { showArrows: true, }; export default Preview; ================================================ FILE: frontend/src/components/Preview/index.js ================================================ import Preview from './Preview'; export default Preview; ================================================ FILE: frontend/src/components/Wrapped/Organization.js ================================================ /* eslint-disable jsx-a11y/mouse-events-have-key-events */ import React from 'react'; import PropTypes from 'prop-types'; import { classnames } from '../../utils'; const WrappedSection = (props) => { return (
{props.useTitle && (

{props.title}

)} {props.children}
); }; WrappedSection.propTypes = { useTitle: PropTypes.bool, title: PropTypes.string, children: PropTypes.node.isRequired, }; WrappedSection.defaultProps = { useTitle: true, title: '', }; const WrappedCard = (props) => { return (
{props.children}
); }; WrappedCard.propTypes = { children: PropTypes.node.isRequired, className: PropTypes.string, onMouseOver: PropTypes.func, onMouseOut: PropTypes.func, }; WrappedCard.defaultProps = { className: '', onMouseOver: () => {}, onMouseOut: () => {}, }; export { WrappedSection, WrappedCard }; ================================================ FILE: frontend/src/components/Wrapped/Specifics/Bar.js ================================================ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { WrappedCard } from '../Organization'; import { BarGraph } from '../Templates'; const monthNames = [ 'Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; const dayNames = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ]; const BarMonth = ({ data, downloadLoading }) => { const newData = data?.month_data?.months || []; // eslint-disable-next-line no-unused-vars const [displayContribs, setDisplayContribs] = useState(true); return (

Contributions by Month

{displayContribs ? 'By Contribution Count' : 'By LOC Changed'}

{!downloadLoading && (
)}
{displayContribs ? ( d.contribs} legendText="Contributions" /> ) : ( d.formatted_loc_changed.split(' ')[0]} legendText="LOC Changed" /> )}
); }; BarMonth.propTypes = { data: PropTypes.object.isRequired, downloadLoading: PropTypes.bool.isRequired, }; const BarDay = ({ data, downloadLoading }) => { const newData = data?.day_data?.days || []; const [displayContribs, setDisplayContribs] = useState(true); return (

Contributions by Day

{displayContribs ? 'By Contribution Count' : 'By LOC Changed'}

{!downloadLoading && (
)}
{displayContribs ? ( d.contribs} legendText="Contributions" /> ) : ( d.formatted_loc_changed.split(' ')[0]} legendText="LOC Changed" /> )}
); }; BarDay.propTypes = { data: PropTypes.object.isRequired, downloadLoading: PropTypes.bool.isRequired, }; export { BarMonth, BarDay }; ================================================ FILE: frontend/src/components/Wrapped/Specifics/Calendar.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { ResponsiveCalendar } from '@nivo/calendar'; import { Input } from '../../Generic'; import { theme, scale } from '../Templates/theme'; import { WrappedCard } from '../Organization'; const Calendar = ({ data, startDate, endDate, highlightDays, highlightColors, downloadLoading, }) => { const newData = data?.calendar_data?.days || []; const valueOptions = [ { value: 'contribs', label: 'Contributions', disabled: false }, { value: 'commits', label: 'Commits', disabled: false }, { value: 'issues', label: 'Issues', disabled: false }, { value: 'prs', label: 'Pull Requests', disabled: false }, { value: 'reviews', label: 'Reviews', disabled: false }, ]; const [value, setValue] = React.useState(valueOptions[0]); const numEvents = Array.isArray(newData) ? newData.reduce((acc, x) => acc + x[value.value], 0) : 0; let c = 0; const max = Math.max(...newData.map((x) => x[value.value])); const quantiles = [ Math.floor(max * 0.25), Math.floor(max * 0.5), Math.floor(max * 0.75), max, ]; const colorScaleFn = (x) => { const count = (c % 365) + 1; c += 1; const myColorScale = highlightDays.includes(count) ? highlightColors : scale; if (x === 0) { return myColorScale[0]; } if (x <= quantiles[0]) { return myColorScale[1]; } if (x <= quantiles[1]) { return myColorScale[2]; } if (x <= quantiles[2]) { return myColorScale[3]; } return myColorScale[4]; }; return (

Contribution Calendar

{!downloadLoading && ( )}

{`${numEvents} ${value.label}`}

{Array.isArray(newData) && newData.length > 0 ? ( ({ day: item.day, value: item[value.value], }))} from={startDate} to={endDate} emptyColor="#EBEDF0" colors={['#9BE9A8', '#40C463', '#30A14E', '#216E39']} margin={{ top: 10, right: 0, bottom: 0, left: 0 }} monthBorderColor="#ffffff" dayBorderWidth={2} dayBorderColor="#ffffff" colorScale={colorScaleFn} /> ) : (
No data to show
)}
); }; Calendar.propTypes = { data: PropTypes.object.isRequired, startDate: PropTypes.string.isRequired, endDate: PropTypes.string.isRequired, highlightDays: PropTypes.arrayOf(PropTypes.number), highlightColors: PropTypes.arrayOf(PropTypes.string).isRequired, downloadLoading: PropTypes.bool.isRequired, }; Calendar.defaultProps = { highlightDays: [], }; export default Calendar; ================================================ FILE: frontend/src/components/Wrapped/Specifics/Numeric.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { WrappedCard } from '../Organization'; const numericPropTypes = { num: PropTypes.any, label: PropTypes.string.isRequired, }; const numericDefaultProps = { num: 'N/A', }; const NumericPlusLOC = ({ num, label }) => { return (

{`+${num}`}

{label}

); }; NumericPlusLOC.propTypes = numericPropTypes; NumericPlusLOC.defaultProps = numericDefaultProps; const NumericMinusLOC = ({ num, label }) => { return (

{`-${num}`}

{label}

); }; NumericMinusLOC.propTypes = numericPropTypes; NumericMinusLOC.defaultProps = numericDefaultProps; const NumericBothLOC = ({ num1, num2, label }) => { return (

{num1}

/

{num2}

{label}

); }; NumericBothLOC.propTypes = { num1: PropTypes.any, num2: PropTypes.any, label: PropTypes.string.isRequired, }; NumericBothLOC.defaultProps = { num1: 'N/A', num2: 'N/A', }; const NumericBestDay = ({ num, date, label, className, onMouseOver, onMouseOut, }) => { return (

{num} Contributions

on {date}

{label}

); }; NumericBestDay.propTypes = { num: PropTypes.number.isRequired, date: PropTypes.string.isRequired, label: PropTypes.string.isRequired, className: PropTypes.string, onMouseOver: PropTypes.func, onMouseOut: PropTypes.func, }; NumericBestDay.defaultProps = { className: '', onMouseOver: () => {}, onMouseOut: () => {}, }; export { NumericPlusLOC, NumericMinusLOC, NumericBothLOC, NumericBestDay }; ================================================ FILE: frontend/src/components/Wrapped/Specifics/Pie.js ================================================ /* eslint-disable react/jsx-curly-newline */ import React from 'react'; import PropTypes from 'prop-types'; import { PieChart } from '../Templates'; import { WrappedCard } from '../Organization'; const PieLangs = ({ data, downloadLoading }) => { const [useLOCAdded, setUseLOCAdded] = React.useState(false); const metric = useLOCAdded ? 'added' : 'changed'; const newData = data?.lang_data?.[`langs_${metric}`] || []; return (

Most Used Languages

{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}

{!downloadLoading && (
)}
e.data.label} getFormattedValue={(e) => e.formatted_value} colors={{ datum: 'data.color' }} />
); }; PieLangs.propTypes = { data: PropTypes.object.isRequired, downloadLoading: PropTypes.bool.isRequired, }; const PieRepos = ({ data, downloadLoading }) => { const [useLOCAdded, setUseLOCAdded] = React.useState(false); const metric = useLOCAdded ? 'added' : 'changed'; const newData = data?.repo_data?.[`repos_${metric}`] || []; return (

Most Active Repositories

{useLOCAdded ? 'By LOC Added' : 'By LOC Changed'}

{!downloadLoading && (
)}
{ if (label && label.includes('/')) { return label.split('/')[1].replace('repository', 'private'); } return label; }} getFormattedValue={(e) => e.formatted_value} colors={{ scheme: 'category10' }} />
); }; PieRepos.propTypes = { data: PropTypes.object.isRequired, downloadLoading: PropTypes.bool.isRequired, }; export { PieLangs, PieRepos }; ================================================ FILE: frontend/src/components/Wrapped/Specifics/Radar.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { ResponsiveRadar } from '@nivo/radar'; import { WrappedCard } from '../Organization'; // eslint-disable-next-line no-unused-vars const Radar = ({ data }) => { const commits = data?.numeric_data?.contribs?.commits || 0; const issues = data?.numeric_data?.contribs?.issues || 0; const prs = data?.numeric_data?.contribs?.prs || 0; const reviews = data?.numeric_data?.contribs?.reviews || 0; const tempData = [ { name: 'Commits', count: Math.log(1 + commits), }, { name: 'Issues', count: Math.log(1 + issues), }, { name: 'Pull Requests', count: Math.log(1 + prs), }, { name: 'Reviews', count: Math.log(1 + reviews), }, ]; return (

Contributions by Type

Log Scale

Math.round(Math.exp(d) - 1)} margin={{ top: 30, right: 50, bottom: 30, left: 60 }} dotSize={10} colors={{ scheme: 'category10' }} blendMode="multiply" motionConfig="wobbly" />
); }; Radar.propTypes = { data: PropTypes.object.isRequired, }; export default Radar; ================================================ FILE: frontend/src/components/Wrapped/Specifics/Swarm.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { SwarmPlot } from '../Templates'; const formatYAxis = (value) => { if (value === 3600 * 12) { return 'Noon'; } if (value === 3600 * 24) { return 'Midnight'; } let hours = Math.floor(value / 3600); const suffix = hours % 24 >= 12 ? 'PM' : 'AM'; hours = hours % 12 === 0 ? 12 : hours % 12; const minutes = String(Math.floor((value % 3600) / 60 / 10) * 10); const displayHour = String(hours).padStart(2, '0'); const displayMinute = String(minutes).padStart(2, '0'); return `${displayHour}:${displayMinute} ${suffix}`; }; const SwarmDay = ({ data }) => { let newData = data?.timestamp_data?.contribs || []; newData = newData.map((d, i) => { return { ...d, groupById: 0, id: i, }; }); return ( ''} formatYAxis={formatYAxis} /> ); }; SwarmDay.propTypes = { data: PropTypes.object.isRequired, }; export { SwarmDay }; ================================================ FILE: frontend/src/components/Wrapped/Specifics/index.js ================================================ import Calendar from './Calendar'; export * from './Bar'; export * from './Numeric'; export * from './Pie'; export * from './Swarm'; export { Calendar }; ================================================ FILE: frontend/src/components/Wrapped/Templates/Bar.js ================================================ /* eslint-disable react/jsx-curly-newline */ import React from 'react'; import PropTypes from 'prop-types'; import { ResponsiveBar } from '@nivo/bar'; import { theme } from './theme'; const BarGraph = ({ data, labels, xTitle, type, getLabel, legendText }) => { const maxData = Math.max(...data.map((d) => d[type])); const minData = Math.min( ...data.filter((d) => d.index < 11).map((d) => d[type]), ); const getColor = (d) => { // eslint-disable-next-line no-nested-ternary return d.value === maxData ? '#2BA02C' : d.value === minData ? '#D62728' : '#468CBF'; }; if (!(Array.isArray(data) && data.length > 0)) { return (
No data to show
); } return ( labels[value], }} axisLeft={{ tickSize: 5, tickPadding: 5, tickRotation: 0, legend: legendText, legendPosition: 'middle', legendOffset: -60, }} label={(d) => getLabel(d.data)} labelSkipWidth={12} labelSkipHeight={12} labelTextColor="#fff" tooltip={() => null} /> ); }; BarGraph.propTypes = { data: PropTypes.array.isRequired, labels: PropTypes.array.isRequired, xTitle: PropTypes.string.isRequired, type: PropTypes.string.isRequired, getLabel: PropTypes.func.isRequired, legendText: PropTypes.string.isRequired, }; export default BarGraph; ================================================ FILE: frontend/src/components/Wrapped/Templates/Numeric.js ================================================ /* eslint-disable jsx-a11y/mouse-events-have-key-events */ import React from 'react'; import PropTypes from 'prop-types'; import { ResponsivePie } from '@nivo/pie'; import { WrappedCard } from '../Organization'; const Numeric = ({ num, label }) => { return (

{num || 'N/A'}

{label}

); }; Numeric.propTypes = { num: PropTypes.any, label: PropTypes.string.isRequired, }; Numeric.defaultProps = { num: 'N/A', }; const NumericOutOf = ({ num, outOf, format, label, color, className, onMouseOver, onMouseOut, }) => { // eslint-disable-next-line react/prop-types const CenteredMetric = ({ dataWithArc, centerX, centerY }) => { let total = 0; // eslint-disable-next-line react/prop-types dataWithArc.forEach((datum) => { total += datum.id === '1' ? datum.value : 0; }); return ( {format(total)} ); }; return (
null} />

{label}

); }; NumericOutOf.propTypes = { num: PropTypes.number.isRequired, outOf: PropTypes.number.isRequired, format: PropTypes.func, label: PropTypes.string.isRequired, color: PropTypes.string, className: PropTypes.string, onMouseOver: PropTypes.func, onMouseOut: PropTypes.func, }; NumericOutOf.defaultProps = { format: (x) => x, color: '#30A14E', className: '', onMouseOver: () => {}, onMouseOut: () => {}, }; export { Numeric, NumericOutOf }; ================================================ FILE: frontend/src/components/Wrapped/Templates/Pie.js ================================================ /* eslint-disable react/jsx-curly-newline */ import React from 'react'; import PropTypes from 'prop-types'; import { ResponsivePie } from '@nivo/pie'; import { theme } from './theme'; const PieChart = ({ data, getArcLinkLabel, getFormattedValue, colors }) => { if (!(Array.isArray(data) && data.length > 0)) { return (
No data to show
); } return ( getArcLinkLabel(e)} arcLinkLabelsSkipAngle={45} arcLinkLabelsTextOffset={0} arcLinkLabelsTextColor={{ from: 'color' }} arcLinkLabelsDiagonalLength={5} arcLinkLabelsStraightLength={5} arcLinkLabelsThickness={0} // Arc Label Settings arcLabel={(e) => getFormattedValue(e.data)} arcLabelsSkipAngle={45} arcLabelsTextColor="#fff" // Tooltip tooltip={({ datum }) => (
{datum.label} {`: ${getFormattedValue(datum.data)}`}
)} colors={colors} /> ); }; PieChart.propTypes = { data: PropTypes.array.isRequired, getArcLinkLabel: PropTypes.func.isRequired, getFormattedValue: PropTypes.func.isRequired, colors: PropTypes.any.isRequired, }; export default PieChart; ================================================ FILE: frontend/src/components/Wrapped/Templates/Swarm.js ================================================ /* eslint-disable react/prop-types */ /* eslint-disable react/jsx-curly-newline */ import React from 'react'; import PropTypes from 'prop-types'; import { ResponsiveSwarmPlot } from '@nivo/swarmplot'; import { theme } from './theme'; import { WrappedCard } from '../Organization'; const MemoizedResponsiveSwarmPlot = React.memo( ResponsiveSwarmPlot, (prevProps, nextProps) => prevProps.data?.length === nextProps.data?.length, ); const SwarmPlot = ({ header, data, groupBy, groups, legend, formatXAxis, formatYAxis, }) => { const tickValues = [0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => 10800 * i); return (

{header}

{`${data.length} Sampled Contributions, Eastern Time`}

{Array.isArray(data) && data.length > 0 ? ( formatXAxis(value), }} axisLeft={{ orient: 'left', tickSize: 10, tickPadding: 5, tickRotation: 0, legend: 'Time of Day', legendPosition: 'middle', legendOffset: -86, tickValues, format: (value) => formatYAxis(value), }} /> ) : (
No data to show
)}
); }; SwarmPlot.propTypes = { header: PropTypes.string.isRequired, data: PropTypes.array.isRequired, groupBy: PropTypes.string.isRequired, groups: PropTypes.array.isRequired, legend: PropTypes.string.isRequired, formatXAxis: PropTypes.func.isRequired, formatYAxis: PropTypes.func.isRequired, }; export default SwarmPlot; ================================================ FILE: frontend/src/components/Wrapped/Templates/index.js ================================================ import BarGraph from './Bar'; import PieChart from './Pie'; import SwarmPlot from './Swarm'; export * from './Numeric'; export * from './theme'; export { BarGraph, PieChart, SwarmPlot }; ================================================ FILE: frontend/src/components/Wrapped/Templates/theme.js ================================================ export const theme = { fontSize: '12px', fontFamily: 'Segoe UI', }; export const scale = ['#EBEDF0', '#9BE9A8', '#40C463', '#30A14E', '#216E39']; export const hoverScale = [ '#A6C9F5', '#7EC7D1', '#50B5AF', '#48A3A4', '#418A9A', ]; export const singleHoverScale = [ '#468CBF', '#468CBF', '#468CBF', '#468CBF', '#468CBF', ]; ================================================ FILE: frontend/src/components/Wrapped/index.js ================================================ export * from './Organization'; export * from './Templates'; export * from './Specifics'; ================================================ FILE: frontend/src/components/index.js ================================================ import Preview from './Preview'; export * from './Generic'; export * from './Card'; export * from './Home'; export * from './Wrapped'; export { Preview }; ================================================ FILE: frontend/src/constants.js ================================================ /* eslint-disable no-nested-ternary */ export const PROD = process.env.REACT_APP_PROD === 'true'; export const USE_LOGGER = true; export const CLIENT_ID = PROD ? process.env.REACT_APP_PROD_CLIENT_ID : process.env.REACT_APP_DEV_CLIENT_ID; export const MODE = process.env.REACT_APP_MODE; export const REDIRECT_URI = PROD ? MODE === 'trends' ? 'https://www.githubtrends.io/user' : 'https://www.githubtrends.io/user/wrapped' : MODE === 'trends' ? 'http://localhost:3000/user' : 'http://localhost:3000/user/wrapped'; export const GITHUB_PRIVATE_AUTH_URL = `https://github.com/login/oauth/authorize?scope=user,repo&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}/private`; export const GITHUB_PUBLIC_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}/public`; export const WRAPPED_URL = PROD ? 'https://www.githubwrapped.io' : 'http://localhost:3001'; export const BACKEND_URL = PROD ? 'https://api.githubtrends.io' : 'http://localhost:8000'; export const CURR_YEAR = 2024; ================================================ FILE: frontend/src/index.css ================================================ /* ./src/index.css */ @tailwind base; @tailwind components; @tailwind utilities; body { margin: 0; font-family: 'Segoe UI', Ubuntu, Sans-Serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ================================================ FILE: frontend/src/index.js ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; import configureStore from './redux/store'; import { AppTrends, AppWrapped } from './pages/App'; import './index.css'; import { MODE } from './constants'; export const store = configureStore(); const root = ReactDOM.createRoot(document.getElementById('root')); if (MODE === 'trends') { root.render( , ); } else if (MODE === 'wrapped') { root.render( , ); } else { // Throw an error if the mode is not set correctly. throw new Error( 'REACT_APP_MODE must be set to "trends" or "wrapped" in your .env file.', ); } ================================================ FILE: frontend/src/pages/App/AppTrends.js ================================================ /* eslint-disable jsx-a11y/anchor-is-valid */ import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { BrowserRouter as Router, Routes, Route, useParams, } from 'react-router-dom'; import Header from './Header'; import LandingScreen from '../Landing'; import DemoScreen from '../Demo'; import { SignUpScreen } from '../Auth'; import HomeScreen from '../Home'; import SettingsScreen from '../Settings'; import { NoMatchScreen, RedirectScreen } from '../Misc'; import { setPrivateAccess as _setPrivateAccess } from '../../redux/actions/userActions'; import { getUserMetadata } from '../../api'; import { WRAPPED_URL } from '../../constants'; import Footer from './Footer'; function WrappedAuthRedirectScreen() { // for wrapped auth redirects const { rest } = useParams(); useEffect(() => { const code = new URLSearchParams(window.location.search).get('code'); window.location.href = `${WRAPPED_URL}/${rest}?code=${code}`; }, [rest]); return null; } function WrappedRedirectScreen() { // redirects /wrapped/* to https://www.githubwrapped.com/* const { userId, year } = useParams(); useEffect(() => { if (userId) { if (year) { window.location.href = `${WRAPPED_URL}/${userId}/${year}`; } else { window.location.href = `${WRAPPED_URL}/${userId}`; } } else { window.location.href = `${WRAPPED_URL}/`; } }, [userId, year]); } function App() { const userId = useSelector((state) => state.user.userId); const isAuthenticated = userId && userId.length > 0; const dispatch = useDispatch(); const setPrivateAccess = (access) => dispatch(_setPrivateAccess(access)); useEffect(() => { async function getPrivateAccess() { if (userId && userId.length > 0) { const result = await getUserMetadata(userId); if (result !== null && result.private_access !== undefined) { setPrivateAccess(result.private_access); } } } getPrivateAccess(); }, [userId]); return (
{!isAuthenticated && ( } /> )} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
); } export default App; ================================================ FILE: frontend/src/pages/App/AppWrapped.js ================================================ /* eslint-disable jsx-a11y/anchor-is-valid */ import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Header from './Header'; import { SignUpScreen } from '../Auth'; import { SelectUserScreen, WrappedScreen } from '../Wrapped'; import { NoMatchScreen } from '../Misc'; import { setPrivateAccess as _setPrivateAccess } from '../../redux/actions/userActions'; import { getUserMetadata } from '../../api'; import Footer from './Footer'; function App() { const userId = useSelector((state) => state.user.userId); const isAuthenticated = userId && userId.length > 0; const dispatch = useDispatch(); const setPrivateAccess = (access) => dispatch(_setPrivateAccess(access)); useEffect(() => { async function getPrivateAccess() { if (userId && userId.length > 0) { const result = await getUserMetadata(userId); if (result !== null && result.private_access !== undefined) { setPrivateAccess(result.private_access); } } } getPrivateAccess(); }, [userId]); return (
{!isAuthenticated && ( } /> )} } /> } /> } /> } /> } /> } />
); } export default App; ================================================ FILE: frontend/src/pages/App/Footer.js ================================================ import React from 'react'; import { CURR_YEAR } from '../../constants'; function Footer() { return (

{`© ${CURR_YEAR} GitHub Trends`}

); } export default Footer; ================================================ FILE: frontend/src/pages/App/Header.js ================================================ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { GiHamburgerMenu as HamburgerIcon } from 'react-icons/gi'; import { MdSettings as SettingsIcon } from 'react-icons/md'; import { logout as _logout } from '../../redux/actions/userActions'; import rocketIcon from '../../assets/rocket.png'; import { classnames } from '../../utils'; import { GITHUB_PUBLIC_AUTH_URL, WRAPPED_URL } from '../../constants'; const propTypes = { to: PropTypes.string.isRequired, children: PropTypes.node.isRequired, onClick: PropTypes.func, className: PropTypes.string, }; const defaultProps = { onClick: null, className: null, }; const StandardLink = ({ to, children, onClick, className }) => ( {children} ); StandardLink.propTypes = propTypes; StandardLink.defaultProps = defaultProps; const MobileLink = ({ to, children, onClick, className }) => ( {children} ); MobileLink.propTypes = propTypes; MobileLink.defaultProps = defaultProps; const Header = ({ mode }) => { const [toggle, setToggle] = useState(false); const userId = useSelector((state) => state.user.userId); const isAuthenticated = userId && userId.length > 0; const dispatch = useDispatch(); const logout = () => dispatch(_logout()); return (
{/* GitHub Trends Logo */} logo {mode === 'trends' && ( GitHub Trends )} {mode === 'wrapped' && ( GitHub Wrapped )} {/* Pages: Wrapped, Dashboard, Demo */} {mode === 'trends' && (
Wrapped {isAuthenticated ? ( Dashboard ) : ( Demo )}
)} {/* Auth Pages: Sign Up, Log In, Log Out */}
{isAuthenticated ? ( <> {mode === 'trends' && ( )} Sign Out ) : ( <> Login Sign Up )}
{/* Hamburger Menu */}
{/* Hamburger Dropdown */}
{mode === 'trends' && ( <> setToggle(false)}> Wrapped {isAuthenticated ? ( setToggle(false)}> Dashboard ) : ( setToggle(false)}> Demo )} )} {isAuthenticated ? ( <> {mode === 'trends' && ( setToggle(false)}> Settings )} { setToggle(false); logout(); }} > Sign Out ) : ( <> Login setToggle(false)} className="block text-sm px-2 my-2 py-2 rounded-sm bg-blue-500 text-white" > Sign Up )}
); }; Header.propTypes = { mode: PropTypes.string.isRequired, }; export default Header; ================================================ FILE: frontend/src/pages/App/index.js ================================================ import AppTrends from './AppTrends'; import AppWrapped from './AppWrapped'; export { AppTrends, AppWrapped }; ================================================ FILE: frontend/src/pages/Auth/SignUp.js ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import { FaGithub as GithubIcon } from 'react-icons/fa'; import { Button } from '../../components'; import { GITHUB_PUBLIC_AUTH_URL, GITHUB_PRIVATE_AUTH_URL, } from '../../constants'; import { classnames } from '../../utils'; import mockup from '../../assets/mockup.png'; const SignUpScreen = () => { // eslint-disable-next-line no-unused-vars const userId = useSelector((state) => state.user.userId); return (
preview
); }; export default SignUpScreen; ================================================ FILE: frontend/src/pages/Auth/index.js ================================================ import SignUpScreen from './SignUp'; export { SignUpScreen }; ================================================ FILE: frontend/src/pages/Demo/Demo.js ================================================ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { Button, SvgInline } from '../../components'; import { getValidUser } from '../../api/wrapped'; import { BACKEND_URL } from '../../constants'; import { classnames } from '../../utils'; const DemoScreen = () => { const [userName, setUserName] = useState(''); const [selectedUserName, setSelectedUserName] = useState(''); const [loading, setLoading] = useState(false); let userNameInput; useEffect(() => { userNameInput.focus(); }, [userNameInput]); const [error, setError] = useState(''); const handleSubmit = async () => { const validUser = await getValidUser(userName); if (validUser.includes('Valid user') || validUser === 'Repo not starred') { setLoading(true); setSelectedUserName(userName); setLoading(false); } else if (validUser === 'GitHub user not found') { setError('GitHub user not found. Check your spelling and try again.'); } }; const firstCardUrl = selectedUserName.length > 0 ? `${BACKEND_URL}/user/svg/${selectedUserName}/langs?demo=true` : `${BACKEND_URL}/user/svg/demo?card=langs`; const secondCardUrl = selectedUserName.length > 0 ? `${BACKEND_URL}/user/svg/${selectedUserName}/repos?demo=true` : `${BACKEND_URL}/user/svg/demo?card=repos`; return (

GitHub Trends Demo

This is a demo of the GitHub Trends API. Enter your GitHub username to see statistics about your top languages and repositories from the past month.

Enter your GitHub username to get started!

{ userNameInput = input; }} placeholder="Enter Username" className={classnames( 'bg-white text-gray-700 w-full input input-bordered rounded-sm', error && 'input-error', )} onChange={(e) => { setUserName(e.target.value); setError(''); }} onKeyPress={async (e) => { if (e.key === 'Enter') { handleSubmit(); } }} />
{error ? (
Error: {error}
) : (
)}

This demo uses a public access token that is heavily rate limited. For full customization, private contributions, and a personal access token, create an account instead!

{selectedUserName === '' ? 'Enter a Username' : `Example Cards for ${selectedUserName}`}

); }; export default DemoScreen; ================================================ FILE: frontend/src/pages/Demo/index.js ================================================ import DemoScreen from './Demo'; export default DemoScreen; ================================================ FILE: frontend/src/pages/Home/Home.js ================================================ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import BounceLoader from 'react-spinners/BounceLoader'; import { FaGithub as GithubIcon } from 'react-icons/fa'; import { ProgressBar } from '../../components'; import { SelectCardStage, CustomizeStage, ThemeStage, DisplayStage, } from './stages'; import { setUserKey, authenticate } from '../../api'; import { login as _login } from '../../redux/actions/userActions'; import { PROD } from '../../constants'; const HomeScreen = () => { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const userId = useSelector((state) => state.user.userId); const privateAccess = useSelector((state) => state.user.privateAccess); const isAuthenticated = userId && userId.length > 0; const dispatch = useDispatch(); const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey)); // for all stages const [stage, setStage] = useState(0); // for stage one const [selectedCard, setSelectedCard] = useState('langs'); // for stage two const defaultTimeRange = { id: 3, label: 'Past 1 Year', disabled: false, value: 'one_year', }; const [selectedTimeRange, setSelectedTimeRange] = useState(defaultTimeRange); const [usePercent, setUsePercent] = useState(false); const [usePrivate, setUsePrivate] = useState(false); const [groupOther, setGroupOther] = useState(false); const [groupPrivate, setGroupPrivate] = useState(false); const [useLocChanged, setUseLocChanged] = useState(false); const [useCompact, setUseCompact] = useState(false); const resetCustomization = () => { setSelectedTimeRange(defaultTimeRange); setUsePercent(false); setUsePrivate(false); setUseLocChanged(false); setUseCompact(false); }; useEffect(() => { resetCustomization(); }, [selectedCard]); const time = selectedTimeRange.value; let fullSuffix = `${selectedCard}?time_range=${time}`; if (usePercent) { fullSuffix += '&use_percent=True'; } if (usePrivate) { fullSuffix += '&include_private=True'; } if (usePrivate && groupOther && groupPrivate) { fullSuffix += '&group=private'; } else if (groupOther) { fullSuffix += '&group=other'; } if (useLocChanged) { fullSuffix += '&loc_metric=changed'; } if (useCompact) { fullSuffix += '&compact=True'; } // for stage three const [theme, setTheme] = useState('classic'); const themeSuffix = `${fullSuffix}&theme=${theme}`; useEffect(() => { async function redirectCode() { // After requesting Github access, Github redirects back to your app with a code parameter const url = window.location.href; if (url.includes('error=')) { navigate('/'); } // If Github API returns the code parameter if (url.includes('code=')) { const tempPrivateAccess = url.includes('private'); const newUrl = url.split('?code='); const subStr = PROD ? 'githubtrends.io' : 'localhost:3000'; const redirect = `${url.split(subStr)[0]}${subStr}/user`; window.history.pushState({}, null, redirect); setIsLoading(true); const userKey = await setUserKey(newUrl[1]); const newUserId = await authenticate(newUrl[1], tempPrivateAccess); login(newUserId, userKey); setIsLoading(false); } } redirectCode(); }, []); if (isLoading) { return (
); } if (!isAuthenticated) { return (

Please sign in to access this page

); } return (
{ [ 'Select a Card', 'Modify Card Parameters', 'Choose a Theme', 'Display your Card', ][stage] }
{ [ 'You will be able to customize your card in future steps.', 'Change the date range, include private commits, and more!', 'Choose from one of our predefined themes (more coming soon!)', 'Display your card on GitHub, Twitter, or Linkedin', ][stage] }
{stage === 0 && ( )} {stage === 1 && ( )} {stage === 2 && ( )} {stage === 3 && ( )}
); }; export default HomeScreen; ================================================ FILE: frontend/src/pages/Home/index.js ================================================ import HomeScreen from './Home'; export default HomeScreen; ================================================ FILE: frontend/src/pages/Home/stages/Customize.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Image, DateRangeSection, CheckboxSection } from '../../../components'; const CustomizeStage = ({ selectedCard, selectedTimeRange, setSelectedTimeRange, usePrivate, setUsePrivate, groupOther, setGroupOther, groupPrivate, setGroupPrivate, privateAccess, useCompact, setUseCompact, usePercent, setUsePercent, useLocChanged, setUseLocChanged, fullSuffix, }) => { return (
{selectedCard === 'langs' && ( )} {selectedCard === 'repos' && ( )} {selectedCard === 'repos' && usePrivate && groupOther && ( )} {selectedCard === 'langs' && ( )}
); }; CustomizeStage.propTypes = { selectedCard: PropTypes.string.isRequired, selectedTimeRange: PropTypes.object.isRequired, setSelectedTimeRange: PropTypes.func.isRequired, usePrivate: PropTypes.bool.isRequired, setUsePrivate: PropTypes.func.isRequired, groupOther: PropTypes.bool.isRequired, setGroupOther: PropTypes.func.isRequired, groupPrivate: PropTypes.bool.isRequired, setGroupPrivate: PropTypes.func.isRequired, privateAccess: PropTypes.bool.isRequired, useCompact: PropTypes.bool.isRequired, setUseCompact: PropTypes.func.isRequired, usePercent: PropTypes.bool.isRequired, setUsePercent: PropTypes.func.isRequired, useLocChanged: PropTypes.bool.isRequired, setUseLocChanged: PropTypes.func.isRequired, fullSuffix: PropTypes.string.isRequired, }; export default CustomizeStage; ================================================ FILE: frontend/src/pages/Home/stages/Display.js ================================================ /* eslint-disable react/no-array-index-key */ import React from 'react'; import PropTypes from 'prop-types'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { saveSvgAsPng } from 'save-svg-as-png'; import { Card, Button } from '../../../components'; import { classnames } from '../../../utils'; const DisplayStage = ({ userId, themeSuffix }) => { const card = themeSuffix.split('?')[0]; const downloadPNG = () => { saveSvgAsPng(document.getElementById('svg-card'), `${userId}_${card}.png`, { scale: 2, encoderOptions: 1, }); }; const copyUrl = () => { navigator.clipboard.writeText( `https://api.githubtrends.io/user/svg/${userId}/${themeSuffix}`, ); toast.info('Copied to Clipboard!', { position: 'bottom-right', autoClose: 1000, hideProgressBar: true, closeOnClick: false, pauseOnHover: false, draggable: false, progress: undefined, }); }; return (
Copy the image URL or download the PNG. Share on GitHub, Twitter, LinkedIn, or anywhere else!

{[ { title: 'Copy URL', active: true, onClick: copyUrl }, { title: 'Download PNG', active: true, onClick: downloadPNG }, ].map((item, index) => ( ))}
); }; DisplayStage.propTypes = { userId: PropTypes.string.isRequired, themeSuffix: PropTypes.string.isRequired, }; export default DisplayStage; ================================================ FILE: frontend/src/pages/Home/stages/SelectCard.js ================================================ /* eslint-disable react/no-array-index-key */ import React from 'react'; import PropTypes from 'prop-types'; import { Card } from '../../../components'; const SelectCardStage = ({ selectedCard, setSelectedCard }) => { return (
{[ { title: 'Language Contributions', description: 'See your overall language breakdown', imageSrc: 'langs', }, { title: 'Repository Contributions', description: 'See your most contributed repositories', imageSrc: 'repos', }, ].map((card, index) => ( ))}
); }; SelectCardStage.propTypes = { selectedCard: PropTypes.string.isRequired, setSelectedCard: PropTypes.func.isRequired, }; export default SelectCardStage; ================================================ FILE: frontend/src/pages/Home/stages/Theme.js ================================================ /* eslint-disable react/no-array-index-key */ import React from 'react'; import PropTypes from 'prop-types'; import { Card } from '../../../components'; const ThemeStage = ({ theme, setTheme, fullSuffix }) => { return (
{[ { title: 'Classic', imageSrc: 'classic', }, { title: 'Dark', imageSrc: 'dark', }, { title: 'Bright Lights', imageSrc: 'bright_lights', }, { title: 'Rosettes', imageSrc: 'rosettes', }, { title: 'Ferns', imageSrc: 'ferns', }, { title: 'Synthwaves', imageSrc: 'synthwaves', }, ].map((card, index) => ( ))}
); }; ThemeStage.propTypes = { theme: PropTypes.string.isRequired, setTheme: PropTypes.func.isRequired, fullSuffix: PropTypes.string.isRequired, }; export default ThemeStage; ================================================ FILE: frontend/src/pages/Home/stages/index.js ================================================ import SelectCardStage from './SelectCard'; import CustomizeStage from './Customize'; import ThemeStage from './Theme'; import DisplayStage from './Display'; export { SelectCardStage, CustomizeStage, ThemeStage, DisplayStage }; ================================================ FILE: frontend/src/pages/Landing/Landing.js ================================================ /* eslint-disable react/jsx-one-expression-per-line */ import React from 'react'; import { useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { FaGithub as GithubIcon, FaCheck as CheckIcon } from 'react-icons/fa'; import { Button, Preview } from '../../components'; import mockup from '../../assets/mockup.png'; import wrapped from '../../assets/wrapped1.png'; import avgupta456Langs from '../../assets/avgupta456_langs.png'; import tiangoloRepos from '../../assets/tiangolo_repos.png'; import reininkRepos from '../../assets/reinink_repos.png'; import dhermesLangs from '../../assets/dhermes_langs.png'; import { WRAPPED_URL } from '../../constants'; function LandingScreen() { const userId = useSelector((state) => state.user.userId); const isAuthenticated = userId && userId.length > 0; return (
Discover and share code contribution insights
GitHub Trends dives deep into the GitHub API to bring you insightful metrics on your contributions, broken by repository and language.
preview

Display your GitHub stats
with embeddable cards

1. Comprehensive

GitHub Trends counts each individual commit, across all your open-source contributions. We surface line of code metrics by repository and languages.


2. Customizable

Using the online dashboard, easily modify the time range, include private commits, and choose your display theme.


3. Shareable

Share your GitHub Trends cards as a PNG on Twitter, or as a dynamic embeddable image on your GitHub profile or personal website.


{!isAuthenticated && ( )}

Reflect on your year
with GitHub Wrapped

1. Detailed

GitHub Wrapped provides a breakdown of your contributions by date, by date, time, repository, and language. Over 20 stats are displayed.


2. Visual

Understand your coding contributions like never before with an interactive calendar, bar charts, pie charts, and more.


3. Public

Share your GitHub Wrapped link with your friends and colleagues and take a look at their contributions too.{' '} No account required, although rate limiting may apply.


preview

GitHub Trends

GitHub Trends dives deep into the GitHub API to bring you insightful metrics and visualizations. We access individual commits to compute accurate and granular statistics.

{[ { header: 'Measures Contribs', text: 'Calculates your stats on a per-contribution level, allowing for deeper insights', }, { header: 'LOC Insights', text: 'See aggregate stats on lines of code (LOC) written across all contributions', }, { header: 'Language Breakdowns', text: 'Showcase your favorite languages with LOC language breakdowns', }, { header: 'Private Mode', text: 'Use a PAT to avoid rate limiting and include private contributions', }, { header: 'Exciting Visualizations', text: 'Visualize your contributions with bar graphs, pie charts, and more', }, { header: 'Shareable Stats', text: 'Easily add your cards to your GitHub and share them online', }, ].map((item, index) => ( // eslint-disable-next-line react/no-array-index-key

{item.header}

{item.text}

))}
{!isAuthenticated && (

Ready to get started?

Create an account today.

)}
); } export default LandingScreen; ================================================ FILE: frontend/src/pages/Landing/index.js ================================================ import LandingScreen from './Landing'; export default LandingScreen; ================================================ FILE: frontend/src/pages/Misc/NoMatch.js ================================================ import React from 'react'; import { Link } from 'react-router-dom'; import { Button } from '../../components'; const NoMatchScreen = () => { return (
404
Page not Found
Please check the URL in the address bar and try again.
); }; export default NoMatchScreen; ================================================ FILE: frontend/src/pages/Misc/Redirect.js ================================================ // eslint-disable-next-line no-unused-vars import React, { useEffect } from 'react'; import { BACKEND_URL } from '../../constants'; const RedirectScreen = () => { useEffect(() => { async function redirectCode() { // Take any query parameters and pass to redirect const url = window.location.href; const hasCode = url.includes('user/redirect'); // If Github API returns the code parameter if (hasCode) { const newUrl = url.split('user/redirect'); window.location.replace(`${BACKEND_URL}/auth/redirect${newUrl[1]}`); } } redirectCode(); }, []); return null; }; export default RedirectScreen; ================================================ FILE: frontend/src/pages/Misc/index.js ================================================ import NoMatchScreen from './NoMatch'; import RedirectScreen from './Redirect'; export { NoMatchScreen, RedirectScreen }; ================================================ FILE: frontend/src/pages/Settings/Settings.js ================================================ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useSelector, useDispatch } from 'react-redux'; import { Button } from '../../components'; import { logout as _logout } from '../../redux/actions/userActions'; import { deleteAccount } from '../../api'; import { classnames } from '../../utils'; import { GITHUB_PRIVATE_AUTH_URL, CLIENT_ID } from '../../constants'; const SectionButton = ({ name, implemented, isSelected, setSelected }) => { return (
setSelected('accountTier')} > {name}
); }; SectionButton.propTypes = { name: PropTypes.string.isRequired, implemented: PropTypes.bool, isSelected: PropTypes.bool.isRequired, setSelected: PropTypes.func.isRequired, }; SectionButton.defaultProps = { implemented: true, }; function useOutsideAlerter(ref, action) { useEffect(() => { /** * Alert if clicked on outside of element */ function handleClickOutside(event) { if (ref.current && !ref.current.contains(event.target)) { action(); } } // Bind the event listener document.addEventListener('mousedown', handleClickOutside); return () => { // Unbind the event listener on clean up document.removeEventListener('mousedown', handleClickOutside); }; }, [ref]); } const SettingsScreen = () => { const [selected, setSelected] = useState('accountTier'); const [deleteModal, setDeleteModal] = useState(false); const openDeleteModal = () => { setDeleteModal(true); }; const closeDeleteModal = () => { setDeleteModal(false); }; const wrapperRef = useRef(null); useOutsideAlerter(wrapperRef, closeDeleteModal); const userId = useSelector((state) => state.user.userId); const isAuthenticated = userId && userId.length > 0; const userKey = useSelector((state) => state.user.userKey); const privateAccess = useSelector((state) => state.user.privateAccess); const accountTier = privateAccess ? 'Private Workflow' : 'Public Workflow'; const dispatch = useDispatch(); const logout = () => dispatch(_logout()); console.log(isAuthenticated, userId, userKey, privateAccess, accountTier); if (!isAuthenticated) { return (

Please sign in to access this page

); } return (

Account Settings


setSelected('accountTier')} /> setSelected('personalization')} /> setSelected('deleteAccount')} />
{selected === 'accountTier' && (

Account Tier



Current Tier: {accountTier}


{privateAccess ? (

You have given GitHub Trends (read and write) access to all public and private code contributions. We use your GitHub API access token to make requests on your behalf. All of our code is open-source and visible on our GitHub repository

) : (

You have given GitHub Trends read access to your public repositories. Upgrading to the Private Workflow will allow us to better represent your code contributions. We use your GitHub API access token to make requests on your behalf. All of our code is open-source and visible on our GitHub repository

)}
{privateAccess ? ( ) : ( )}
)} {selected === 'personalization' && (

Personalization



Coming soon!

)} {selected === 'deleteAccount' && (

Delete Account



Deleting your account is permanent and cannot be undone. If you are sure you want to delete your account, click the button below to remove your statistics from GitHub Trends. This will redirect you to a GitHub screen where you can revoke your access token.


)}
{deleteModal && (

Delete Account



Are you sure you want to continue? This action cannot be undone.


)}
); }; export default SettingsScreen; ================================================ FILE: frontend/src/pages/Settings/index.js ================================================ import SettingsScreen from './Settings'; export default SettingsScreen; ================================================ FILE: frontend/src/pages/Wrapped/SelectUser.js ================================================ /* eslint-disable no-alert */ /* eslint-disable no-unused-vars */ import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, Link } from 'react-router-dom'; import { ClipLoader } from 'react-spinners'; import { BsInfoCircle } from 'react-icons/bs'; import { FaGithub as GithubIcon, FaCheck as CheckIcon } from 'react-icons/fa'; import { getValidUser } from '../../api/wrapped'; import { Button, Preview } from '../../components'; import { classnames, sleep } from '../../utils'; import wrapped1 from '../../assets/wrapped1.png'; import wrapped2 from '../../assets/wrapped2.png'; import wrapped3 from '../../assets/wrapped3.png'; import { PROD } from '../../constants'; import { authenticate, setUserKey } from '../../api'; import { login as _login } from '../../redux/actions/userActions'; const SelectUserScreen = () => { const userId = useSelector((state) => state.user.userId || ''); const [userName, setUserName] = useState(userId); const navigate = useNavigate(); const dispatch = useDispatch(); let userNameInput; useEffect(() => { userNameInput.focus(); }, [userNameInput]); const login = (newUserId, userKey) => dispatch(_login(newUserId, userKey)); useEffect(() => { async function redirectCode() { // After requesting Github access, Github redirects back to your app with a code parameter const url = window.location.href; if (url.includes('error=')) { navigate('/'); } // If Github API returns the code parameter if (url.includes('code=')) { const tempPrivateAccess = url.includes('private'); const newUrl = url.split('?code='); const subStr = PROD ? 'githubwrapped.io' : 'localhost:3001'; const redirect = `${url.split(subStr)[0]}${subStr}/`; window.history.pushState({}, null, redirect); const userKey = await setUserKey(newUrl[1]); const newUserId = await authenticate(newUrl[1], tempPrivateAccess); login(newUserId, userKey); } } redirectCode(); }, []); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSubmit = async () => { setIsLoading(true); const validUser = await getValidUser(userName); if (validUser.includes('Valid user')) { const newUserName = validUser.split(' ')[2]; await sleep(10); navigate(`/${newUserName}`); } else if (validUser === 'GitHub user not found') { setError('GitHub user not found. Check your spelling and try again.'); } else if (validUser === 'Repo not starred') { setError( 'This user has not starred the GitHub Trends repository. Please star the repo and try again.', ); } setIsLoading(false); }; return (

Reflect on your year
with GitHub Wrapped

Powered by{' '} GitHub Trends {' '} (not affiliated with GitHub)

Step 1: Star the GitHub repository.{' '}

Step 2: Enter your GitHub username!

{ userNameInput = input; }} placeholder="Enter Username" className={classnames( 'bg-white text-gray-700 w-full input input-bordered rounded-sm', error && 'input-error', )} defaultValue={userName} onChange={(e) => { setUserName(e.target.value); setError(''); }} onKeyPress={async (e) => { if (e.key === 'Enter') { handleSubmit(); } }} />
{error ? (
Error: {error}
) : (
)}

See some examples

{[ { name: 'Linus Torvalds', username: 'torvalds', url: 'https://avatars.githubusercontent.com/u/1024025?v=4', blurb: 'Creator of Linux', }, { name: 'Evan You', username: 'yyx990803', url: 'https://avatars.githubusercontent.com/u/499550?v=4', blurb: 'Creator of Vue', }, { name: 'shadcn', username: 'shadcn', url: 'https://avatars.githubusercontent.com/u/124599?v=4', blurb: 'Vercel, shadcn/ui', }, { name: 'Sindre Sorhus', username: 'sindresorhus', url: 'https://avatars.githubusercontent.com/u/170270?v=4', blurb: 'Open-sourcer', }, ].map((user) => (
{user.username}
{user.name}

{user.blurb}

))}

GitHub Trends

GitHub Trends dives deep into the GitHub API to bring you insightful metrics and visualizations. We access individual commits to compute accurate and granular statistics.

{[ { header: 'Measures Contribs', text: 'Calculates your stats on a per-contribution level, allowing for deeper insights', }, { header: 'LOC Insights', text: 'See aggregate stats on lines of code (LOC) written across all contributions', }, { header: 'Language Breakdowns', text: 'Showcase your favorite languages with LOC language breakdowns', }, { header: 'Private Mode', text: 'Use a PAT to avoid rate limiting and include private contributions', }, { header: 'Exciting Visualizations', text: 'Visualize your contributions with bar graphs, pie charts, and more', }, { header: 'Shareable Stats', text: 'Easily add your cards to your GitHub and share them online', }, ].map((item, index) => ( // eslint-disable-next-line react/no-array-index-key

{item.header}

{item.text}

))}
); }; export default SelectUserScreen; ================================================ FILE: frontend/src/pages/Wrapped/Wrapped.js ================================================ /* eslint-disable react/jsx-one-expression-per-line */ import React, { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { useParams, Link } from 'react-router-dom'; import { toPng } from 'html-to-image'; import download from 'downloadjs'; import { FaArrowLeft as LeftArrowIcon } from 'react-icons/fa'; import { BsImage as ImageIcon, BsInfoCircle } from 'react-icons/bs'; import Select from 'react-select'; import { ClipLoader } from 'react-spinners'; import { getWrapped } from '../../api'; import { WrappedSection, Numeric, NumericOutOf, Calendar, hoverScale, singleHoverScale, BarMonth, BarDay, PieLangs, PieRepos, SwarmDay, NumericPlusLOC, NumericMinusLOC, NumericBothLOC, NumericBestDay, } from '../../components'; import Radar from '../../components/Wrapped/Specifics/Radar'; import { LoadingScreen } from './sections'; import { classnames } from '../../utils'; import { CURR_YEAR } from '../../constants'; const WrappedScreen = () => { // eslint-disable-next-line prefer-const let { userId, year } = useParams(); year = year || `${CURR_YEAR}`; const currUserId = useSelector((state) => state.user.userId); const usePrivate = useSelector((state) => state.user.privateAccess); const [data, setData] = useState({}); const [isLoading, setIsLoading] = useState(true); const [highlightDays, setHighlightDays] = useState([]); const [highlightColors, setHighlightColors] = useState(hoverScale); const [downloadLoading, setDownloadLoading] = useState(false); // eslint-disable-next-line no-unused-vars const downloadImage = async () => { const dataUrl = await toPng(document.getElementById('screenshot-div')); download(dataUrl, 'github-wrapped.png'); }; useEffect(() => { async function getData() { if (userId?.length > 0 && year > 2010 && year <= CURR_YEAR) { const output = await getWrapped(userId, year); if ( output !== null && output !== undefined && Object.keys(output).length > 0 ) { setData(output); setIsLoading(false); } } } getData(); }, [userId, year]); if (isLoading) { return ; } const startStreak = data?.numeric_data?.misc?.longest_streak_days?.[0] || 0; const endStreak = data?.numeric_data?.misc?.longest_streak_days?.[1] || 0; const startGap = data?.numeric_data?.misc?.longest_gap_days?.[0] || 0; const endGap = data?.numeric_data?.misc?.longest_gap_days?.[1] || 0; const bestDayMonth = data?.numeric_data?.misc?.best_day_date?.split('-')?.[1] || '-'; const bestDayDay = data?.numeric_data?.misc?.best_day_date?.split('-')?.[2] || '-'; const bestDayYear = data?.numeric_data?.misc?.best_day_date?.split('-')?.[0] || '-'; return (
{!downloadLoading && ( )}

{`${userId}'s`}