From 11dbb4e357eb5b39fa41a2e58d08530d7142a429 Mon Sep 17 00:00:00 2001 From: Luke Hagar Date: Tue, 12 Aug 2025 10:35:39 -0500 Subject: [PATCH] Saving progress --- .devcontainer/devcontainer.json | 33 + .dockerignore | 54 +- .env.sample | 23 - .gitignore | 168 +- .npmrc | 1 + .prettierignore | 9 + .prettierrc | 16 + .tool-versions | 3 - Dockerfile | 60 +- Makefile | 33 - README.md | 38 + README.rst | 26 - README_DOCKER.md | 24 + docker-compose.dev.yml | 26 + docker-compose.yml | 129 +- docker-entrypoint.sh | 36 - docker/entrypoint.sh | 71 + kubernetes/commands.sh | 30 - kubernetes/deploy.sh | 12 - kubernetes/flower.yaml | 45 - kubernetes/namespace.yaml | 4 - kubernetes/redis.yaml | 36 - kubernetes/tasks.yaml | 35 - kubernetes/web.yaml | 81 - migrations/README | 1 - migrations/alembic.ini | 45 - migrations/env.py | 88 - migrations/script.py.mako | 24 - migrations/seeds.py | 97 - ...200303_221751_0cf9945079f1_setup_tables.py | 95 - package.json | 51 + pnpm-lock.yaml | 2797 +++++++++++++++++ poetry.lock | 1290 -------- .../20250804175350_init/migration.sql | 48 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 70 + pypistats/application.py | 50 - pypistats/config.py | 71 - pypistats/database.py | 58 - pypistats/extensions.py | 24 - pypistats/models/__init__.py | 0 pypistats/models/download.py | 81 - pypistats/models/user.py | 35 - pypistats/plots/data_base.json | 72 - pypistats/plots/plot_base.json | 212 -- pypistats/run.py | 39 - pypistats/tasks/__init__.py | 0 pypistats/tasks/pypi.py | 408 --- pypistats/templates/about.html | 44 - pypistats/templates/admin.html | 20 - pypistats/templates/api.html | 269 -- pypistats/templates/faqs.html | 89 - pypistats/templates/index.html | 24 - pypistats/templates/layout.html | 85 - pypistats/templates/package.html | 105 - pypistats/templates/results.html | 12 - pypistats/templates/search.html | 1 - pypistats/templates/top.html | 37 - pypistats/templates/user.html | 31 - pypistats/views/__init__.py | 6 - pypistats/views/admin.py | 38 - pypistats/views/api.py | 148 - pypistats/views/error.py | 40 - pypistats/views/general.py | 320 -- pypistats/views/user.py | 133 - pyproject.toml | 63 - src/app.css | 3 + src/app.d.ts | 13 + src/app.html | 11 + src/hooks.server.ts | 35 + src/lib/api.ts | 271 ++ src/lib/assets/favicon.svg | 1 + src/lib/data-processor.ts | 742 +++++ src/lib/database-freshness.ts | 144 + src/lib/database.ts | 11 + src/lib/index.ts | 1 + src/lib/prisma.ts | 9 + src/lib/redis.ts | 341 ++ src/routes/+layout.svelte | 51 + src/routes/+page.server.ts | 15 + src/routes/+page.svelte | 71 + src/routes/about/+page.svelte | 42 + src/routes/admin/+page.svelte | 323 ++ .../__init__.py => src/routes/admin/+page.ts | 0 src/routes/api/+page.svelte | 167 + src/routes/api/admin/cache/+server.ts | 88 + src/routes/api/admin/cron/+server.ts | 11 + src/routes/api/admin/process-data/+server.ts | 28 + .../packages/[package]/installer/+server.ts | 33 + .../api/packages/[package]/overall/+server.ts | 35 + .../[package]/python_major/+server.ts | 35 + .../[package]/python_minor/+server.ts | 35 + .../api/packages/[package]/recent/+server.ts | 70 + .../api/packages/[package]/system/+server.ts | 35 + src/routes/faqs/+page.svelte | 79 + src/routes/packages/[package]/+page.svelte | 155 + src/routes/packages/[package]/+page.ts | 59 + src/routes/search/+page.server.ts | 54 + src/routes/search/+page.svelte | 67 + static/robots.txt | 3 + {pypistats/static => static}/style.css | 0 svelte.config.js | 20 + tsconfig.json | 19 + vite.config.ts | 7 + 104 files changed, 6376 insertions(+), 4825 deletions(-) create mode 100644 .devcontainer/devcontainer.json delete mode 100644 .env.sample create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc delete mode 100644 .tool-versions delete mode 100644 Makefile create mode 100644 README.md delete mode 100644 README.rst create mode 100644 README_DOCKER.md create mode 100644 docker-compose.dev.yml delete mode 100755 docker-entrypoint.sh create mode 100644 docker/entrypoint.sh delete mode 100644 kubernetes/commands.sh delete mode 100644 kubernetes/deploy.sh delete mode 100644 kubernetes/flower.yaml delete mode 100644 kubernetes/namespace.yaml delete mode 100644 kubernetes/redis.yaml delete mode 100644 kubernetes/tasks.yaml delete mode 100644 kubernetes/web.yaml delete mode 100755 migrations/README delete mode 100644 migrations/alembic.ini delete mode 100755 migrations/env.py delete mode 100755 migrations/script.py.mako delete mode 100644 migrations/seeds.py delete mode 100644 migrations/versions/20200303_221751_0cf9945079f1_setup_tables.py create mode 100644 package.json create mode 100644 pnpm-lock.yaml delete mode 100644 poetry.lock create mode 100644 prisma/migrations/20250804175350_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma delete mode 100644 pypistats/application.py delete mode 100644 pypistats/config.py delete mode 100644 pypistats/database.py delete mode 100644 pypistats/extensions.py delete mode 100644 pypistats/models/__init__.py delete mode 100644 pypistats/models/download.py delete mode 100644 pypistats/models/user.py delete mode 100644 pypistats/plots/data_base.json delete mode 100644 pypistats/plots/plot_base.json delete mode 100644 pypistats/run.py delete mode 100644 pypistats/tasks/__init__.py delete mode 100644 pypistats/tasks/pypi.py delete mode 100644 pypistats/templates/about.html delete mode 100644 pypistats/templates/admin.html delete mode 100644 pypistats/templates/api.html delete mode 100644 pypistats/templates/faqs.html delete mode 100644 pypistats/templates/index.html delete mode 100644 pypistats/templates/layout.html delete mode 100644 pypistats/templates/package.html delete mode 100644 pypistats/templates/results.html delete mode 100644 pypistats/templates/search.html delete mode 100644 pypistats/templates/top.html delete mode 100644 pypistats/templates/user.html delete mode 100644 pypistats/views/__init__.py delete mode 100644 pypistats/views/admin.py delete mode 100644 pypistats/views/api.py delete mode 100644 pypistats/views/error.py delete mode 100644 pypistats/views/general.py delete mode 100644 pypistats/views/user.py delete mode 100644 pyproject.toml create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/hooks.server.ts create mode 100644 src/lib/api.ts create mode 100644 src/lib/assets/favicon.svg create mode 100644 src/lib/data-processor.ts create mode 100644 src/lib/database-freshness.ts create mode 100644 src/lib/database.ts create mode 100644 src/lib/index.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/redis.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/about/+page.svelte create mode 100644 src/routes/admin/+page.svelte rename pypistats/__init__.py => src/routes/admin/+page.ts (100%) create mode 100644 src/routes/api/+page.svelte create mode 100644 src/routes/api/admin/cache/+server.ts create mode 100644 src/routes/api/admin/cron/+server.ts create mode 100644 src/routes/api/admin/process-data/+server.ts create mode 100644 src/routes/api/packages/[package]/installer/+server.ts create mode 100644 src/routes/api/packages/[package]/overall/+server.ts create mode 100644 src/routes/api/packages/[package]/python_major/+server.ts create mode 100644 src/routes/api/packages/[package]/python_minor/+server.ts create mode 100644 src/routes/api/packages/[package]/recent/+server.ts create mode 100644 src/routes/api/packages/[package]/system/+server.ts create mode 100644 src/routes/faqs/+page.svelte create mode 100644 src/routes/packages/[package]/+page.svelte create mode 100644 src/routes/packages/[package]/+page.ts create mode 100644 src/routes/search/+page.server.ts create mode 100644 src/routes/search/+page.svelte create mode 100644 static/robots.txt rename {pypistats/static => static}/style.css (100%) create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9cf468f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,33 @@ +{ + "name": "pypistats.dev", + "dockerComposeFile": [ + "../docker-compose.yml", + "../docker-compose.dev.yml" + ], + "service": "web", + "workspaceFolder": "/app", + "build": { + "args": { + "SKIP_APP_BUILD": "1" + } + }, + "runArgs": ["--env-file", ".env"], + "forwardPorts": [5173, 3000, 5555], + "portsAttributes": { + "5173": { "label": "Vite Dev Server" }, + "3000": { "label": "Node Adapter Server" }, + "5555": { "label": "Prisma Studio" } + }, + "customizations": { + "vscode": { + "extensions": [ + "esbenp.prettier-vscode", + "Prisma.prisma", + "svelte.svelte-vscode", + "dbaeumer.vscode-eslint" + ] + } + } +} + + diff --git a/.dockerignore b/.dockerignore index f995b72..e0e3b99 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,41 +1,15 @@ -# project .git -.gitignoreold -.dockerignore -.tool-versions -docker-compose.yml -envs/ -.venv/ -scripts/ -kubernetes/ -pypistats.egg-info/ -Dockerfile -Makefile -README.rst - -# mac osx -**/.DS_Store - -# python bytecode -*.py[cod] -**/__pycache__/ - -# celery -celerybeat-schedule -celerybeat.pid - -# redis -dump.rdb - -# Elastic Beanstalk Files -.elasticbeanstalk -.ebignore - -# intellij -.idea/ - -# secrets -*.env -.env.sample - -.gitignore +node_modules +.pnpm-store +.svelte-kit +build +.vscode +.idea +.DS_Store +.devcontainer +.env +.env.* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* \ No newline at end of file diff --git a/.env.sample b/.env.sample deleted file mode 100644 index b2af9e9..0000000 --- a/.env.sample +++ /dev/null @@ -1,23 +0,0 @@ -ENV=development -CELERY_BROKER_URL=redis://redis -FLOWER_PORT=5555 -FLASK_APP=pypistats/run.py -FLASK_DEBUG=1 -GOOGLE_TYPE= -GOOGLE_PROJECT_ID= -GOOGLE_PRIVATE_KEY_ID= -GOOGLE_PRIVATE_KEY= -GOOGLE_CLIENT_EMAIL= -GOOGLE_CLIENT_ID= -GOOGLE_AUTH_URI= -GOOGLE_TOKEN_URI= -GOOGLE_AUTH_PROVIDER_X509_CERT_URL= -GOOGLE_CLIENT_X509_CERT_URL= -POSTGRESQL_HOST=postgresql -POSTGRESQL_PORT=5432 -POSTGRESQL_USERNAME=admin -POSTGRESQL_PASSWORD=root -POSTGRESQL_DBNAME=pypistats -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -PYPISTATS_SECRET=secret diff --git a/.gitignore b/.gitignore index ae91da3..b2240a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,155 +1,25 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +node_modules -# C extensions -*.so +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -envs/.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# MacOS +# OS .DS_Store +Thumbs.db -# Intellij -.idea/ - - -# TODO remove -# EB -.elasticbeanstalk/ -# Creds -envs/ -*.env - +# Env +.env +.env.* +!.env.example +!.env.test +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +/../generated/prisma diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8103a0b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/app.css" +} diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 43519cb..0000000 --- a/.tool-versions +++ /dev/null @@ -1,3 +0,0 @@ -python 3.8.5 -poetry 1.0.10 -kubectl 1.17.4 diff --git a/Dockerfile b/Dockerfile index 4aa47db..56575a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,37 @@ -FROM python:3.8.5-slim +FROM node:20-slim -# Add build deps for python packages -# libpq-dev is required to install psycopg2-binary -# curl is used to install poetry -RUN apt-get update && \ - apt-get install -y curl libpq-dev && \ - apt-get clean +# Install deps needed by Prisma and shell +RUN apt-get update && apt-get install -y openssl bash && rm -rf /var/lib/apt/lists/* -# Set the working directory to /app WORKDIR /app -# Create python user to avoid having to run as root -RUN useradd -m python && \ - chown python:python -R /app -# Set the user -USER python +# Allow skipping app build in devcontainer +ARG SKIP_APP_BUILD=0 -# Set the poetry version -ARG POETRY_VERSION=1.0.10 -# Set to ensure logs are output promptly -ENV PYTHONUNBUFFERED=1 -# Update the path -ENV PATH=/home/python/.poetry/bin:/home/python/.local/bin:$PATH +# Copy package manifests first for better cache +COPY package.json pnpm-lock.yaml* ./ -# Install vendored poetry -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python +# Enable and use pnpm via corepack +RUN corepack enable && corepack prepare pnpm@9.12.3 --activate -# Add poetry stuff -ADD pyproject.toml . -ADD poetry.lock . +# Install dependencies +RUN pnpm install --frozen-lockfile -# Install all the dependencies and cleanup -RUN poetry config virtualenvs.create false && \ - poetry run pip install --user -U pip && \ - poetry install --no-dev && \ - "yes" | poetry cache clear --all pypi +# Copy the rest of the source +COPY . . + +# Generate Prisma client and build SvelteKit (Node adapter) +RUN pnpm prisma generate +RUN if [ "$SKIP_APP_BUILD" != "1" ]; then pnpm build; fi + +ENV NODE_ENV=production + +# Entrypoint handles migrations and start +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 3000 + +ENTRYPOINT ["/entrypoint.sh"] -# Add everything -ADD . . -# Set the entrypoint script -ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 06a90aa..0000000 --- a/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -# format everything -fmt: - poetry run isort . - poetry run black . - -# launch the application in docker-compose -.PHONY: pypistats -pypistats: - docker-compose down - docker-compose build - docker-compose up - -# bring down the application and destroy the db volumes -cleanup: - docker-compose down -v - -# setup a local environment -setup: - brew install asdf || true - asdf install - poetry install - -# deploy to gke -deploy: - sh kubernetes/deploy.sh - -# port forward flower -pfflower: - open http://localhost:7777 && kubectl get pods -n pypistats | grep flower | awk '{print $$1}' | xargs -I % kubectl port-forward -n pypistats % 7777:5555 - -# port forward web -pfweb: - open http://localhost:7000 && kubectl get pods -n pypistats | grep web | awk '{print $$1}' | xargs -I % kubectl port-forward -n pypistats % 7000:5000 diff --git a/README.md b/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/README.rst b/README.rst deleted file mode 100644 index 00536ba..0000000 --- a/README.rst +++ /dev/null @@ -1,26 +0,0 @@ -PyPI Stats -========== - -A simple analytics dashboard for aggregate data on PyPI downloads. PyPI Stats is built using Flask with plotly.js. - -`PyPI Stats `_ - -GitHub OAuth ------------- - -PyPI Stats has an integration with GitHub so you can track install data on the packages you maintain. - -`User page `_ - -JSON API --------- - -PyPI Stats provides a simple JSON API to retrieve aggregate download stats and time histories of pypi packages. - -`JSON API `_ - -Development ------------ - -Run ``make pypistats`` to launch a complete development environment using docker-compose. - diff --git a/README_DOCKER.md b/README_DOCKER.md new file mode 100644 index 0000000..73f7859 --- /dev/null +++ b/README_DOCKER.md @@ -0,0 +1,24 @@ +### Running locally with Docker + +Prerequisites: Docker and Docker Compose. + +1. Build and start the full stack (Postgres, Redis, Web): + +``` +docker compose up --build +``` + +2. Configure BigQuery credentials via environment variables (e.g., export `GOOGLE_PROJECT_ID` and `GOOGLE_APPLICATION_CREDENTIALS_JSON`). For local compose, you can add them under the `web.environment` section in `docker-compose.yml`. + +3. The app runs on `http://localhost:3000`. + +Environment variables of interest: +- `DATABASE_URL`: Postgres connection string. +- `REDIS_URL`: Redis URL. +- `ENABLE_CRON`: Set to `true` to run the daily ETL. +- `CRON_SCHEDULE`: Cron string (default 2 AM UTC daily). +- `GOOGLE_PROJECT_ID`, `GOOGLE_APPLICATION_CREDENTIALS_JSON` or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery. + +The container entrypoint waits for Postgres, applies Prisma migrations, then starts the app. + + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..2bb04f4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,26 @@ +version: '3.9' + +services: + web: + build: + context: . + args: + SKIP_APP_BUILD: "1" + command: sh -lc "\ + corepack enable && corepack prepare pnpm@9.12.3 --activate && \ + pnpm install && \ + pnpm prisma generate && \ + pnpm prisma migrate deploy || true && \ + pnpm dev --host 0.0.0.0 --port 5173" + volumes: + - ./:/app + - web_node_modules:/app/node_modules + env_file: + - .env + ports: + - "5173:5173" + +volumes: + web_node_modules: + + diff --git a/docker-compose.yml b/docker-compose.yml index 0f9f47a..36e685f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,92 +1,53 @@ -x-envs: &envs - environment: - - FLASK_APP=pypistats/run.py - - FLASK_ENV=development - - FLASK_DEBUG=1 - - POSTGRESQL_HOST=postgresql - - POSTGRESQL_PORT=5432 - - POSTGRESQL_USERNAME=admin - - POSTGRESQL_PASSWORD=root - - POSTGRESQL_DBNAME=pypistats - - CELERY_BROKER_URL=redis://redis - - BASIC_AUTH_USER=user - - BASIC_AUTH_PASSWORD=password - -version: "3.4" - -volumes: - pgdata: {} +version: '3.9' services: - web: - build: - context: . - image: web - command: webdev - depends_on: - - postgresql - <<: *envs + db: + image: postgres:16 + environment: + POSTGRES_DB: pypistats + POSTGRES_USER: pypistats + POSTGRES_PASSWORD: pypistats + volumes: + - pgdata:/var/lib/postgresql/data ports: - - "5000:5000" - volumes: - - "./pypistats/:/app/pypistats/" - beat: - image: web - command: beat - depends_on: - - redis - <<: *envs - volumes: - - "./pypistats/:/app/pypistats/" - celery: - image: web - command: celery - depends_on: - - redis - - postgresql - <<: *envs - volumes: - - "./pypistats/:/app/pypistats/" - flower: - image: web - command: flower - depends_on: - - redis - <<: *envs - ports: - - "5555:5555" - volumes: - - "./pypistats/:/app/pypistats/" - migrate: - image: web - command: migrate - depends_on: - - postgresql - <<: *envs - volumes: - - "./pypistats/:/app/pypistats/" - - "./migrations/:/app/migrations/" - seeds: - image: web - command: seeds - depends_on: - - postgresql - - migrate - <<: *envs - volumes: - - "./pypistats/:/app/pypistats/" - - "./migrations/:/app/migrations/" + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 5s + retries: 20 + redis: - image: "redis:5.0.7-alpine" + image: redis:7 ports: - "6379:6379" - postgresql: - image: "postgres:12" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 20 + + web: + build: . + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy environment: - - POSTGRES_USER=admin - - POSTGRES_PASSWORD=root - - POSTGRES_DB=pypistats + NODE_ENV: production + PORT: 3000 + DATABASE_URL: postgresql://pypistats:pypistats@db:5432/pypistats?schema=public + REDIS_URL: redis://redis:6379 + ENABLE_CRON: "true" + # Set your BigQuery project and credentials + # GOOGLE_PROJECT_ID: your-project + # GOOGLE_APPLICATION_CREDENTIALS_JSON: '{"type":"service_account",...}' ports: - - "5433:5432" - volumes: - - "pgdata:/var/lib/postgresql/data" + - "3000:3000" + command: ["/entrypoint.sh"] + +volumes: + pgdata: + + diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100755 index ad80147..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -if [[ "$1" = "webdev" ]] -then - exec poetry run flask run --host 0.0.0.0 -fi - -if [[ "$1" = "web" ]] -then - exec poetry run gunicorn -b 0.0.0.0:5000 -w 2 --access-logfile - --error-log - --access-logformat "%({x-forwarded-for}i)s %(l)s %(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s \"%(f)s\" \"%(a)s\"" pypistats.run:app -fi - -if [[ "$1" = "celery" ]] -then - exec poetry run celery -A pypistats.extensions.celery worker -l info --concurrency=1 -fi - -if [[ "$1" = "beat" ]] -then - exec poetry run celery -A pypistats.extensions.celery beat -l info -fi - -if [[ "$1" = "flower" ]] -then - exec poetry run flower -A pypistats.extensions.celery -l info -fi - -if [[ "$1" = "migrate" ]] -then - exec poetry run flask db upgrade -fi - -if [[ "$1" = "seeds" ]] -then - exec poetry run python -m migrations.seeds -fi \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..1ba5cb7 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wait for Postgres if DATABASE_URL is provided +if [[ -n "${DATABASE_URL:-}" ]]; then + echo "Waiting for database..." + ATTEMPTS=0 + until node -e "const { Client } = require('pg'); (async () => { try { const c=new Client({ connectionString: process.env.DATABASE_URL }); await c.connect(); await c.end(); process.exit(0);} catch(e){ process.exit(1);} })()" >/dev/null 2>&1; do + ATTEMPTS=$((ATTEMPTS+1)) + if [[ $ATTEMPTS -gt 60 ]]; then + echo "Database did not become ready in time" >&2 + exit 1 + fi + sleep 1 + done +fi + +# Run Prisma migrations (safe for prod) with retry +if [[ "${RUN_DB_MIGRATIONS:-1}" == "1" ]]; then + echo "Running prisma migrate deploy..." + ATTEMPTS=0 + until pnpm prisma migrate deploy; do + ATTEMPTS=$((ATTEMPTS+1)) + if [[ $ATTEMPTS -gt 10 ]]; then + echo "Prisma migrate failed after retries" >&2 + exit 1 + fi + echo "Retrying migrations in 3s..." + sleep 3 + done +fi + +# Start the app (SvelteKit Node adapter) +exec node build/index.js + +#!/usr/bin/env bash +set -euo pipefail + +# Wait for Postgres if DATABASE_URL is provided +if [[ -n "${DATABASE_URL:-}" ]]; then + echo "Waiting for database..." + ATTEMPTS=0 + until node -e "const { Client } = require('pg'); (async () => { try { const c=new Client({ connectionString: process.env.DATABASE_URL }); await c.connect(); await c.end(); process.exit(0);} catch(e){ process.exit(1);} })()" >/dev/null 2>&1; do + ATTEMPTS=$((ATTEMPTS+1)) + if [[ $ATTEMPTS -gt 60 ]]; then + echo "Database did not become ready in time" >&2 + exit 1 + fi + sleep 1 + done +fi + +# Run Prisma migrations (safe for prod) with retry +if [[ "${RUN_DB_MIGRATIONS:-1}" == "1" ]]; then + echo "Running prisma migrate deploy..." + ATTEMPTS=0 + until pnpm prisma migrate deploy; do + ATTEMPTS=$((ATTEMPTS+1)) + if [[ $ATTEMPTS -gt 10 ]]; then + echo "Prisma migrate failed after retries" >&2 + exit 1 + fi + echo "Retrying migrations in 3s..." + sleep 3 + done +fi + +# Start the app (SvelteKit Node adapter) +exec node build/index.js + + diff --git a/kubernetes/commands.sh b/kubernetes/commands.sh deleted file mode 100644 index 97af98a..0000000 --- a/kubernetes/commands.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -docker build -t us.gcr.io/pypistats-org/pypistats:$(poetry version | tail -c +14) . -docker push us.gcr.io/pypistats-org/pypistats:$(poetry version | tail -c +14) - -kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml - -# create namespace ``pypistats`` -kubectl apply -f kubernetes/namespace.yaml - -# create secret from the env file -#kubectl delete secret pypistats-secrets --namespace=pypistats -# create -kubectl create secret generic pypistats-secrets --from-env-file=gke.env --namespace=pypistats -# update -kubectl create secret generic pypistats-secrets --from-env-file=gke.env --namespace=pypistats --dry-run -o yaml | kubectl apply -f - - -# create redis and flower -kubectl apply -f kubernetes/redis.yaml --namespace=pypistats -kubectl apply -f kubernetes/flower.yaml --namespace=pypistats - -# launch the web components -kubectl apply -f kubernetes/web.yaml --namespace=pypistats - -# launch the tasks components -kubectl apply -f kubernetes/tasks.yaml --namespace=pypistats - -# get info about connecting -kubectl cluster-info -kubectl get services --namespace=pypistats - diff --git a/kubernetes/deploy.sh b/kubernetes/deploy.sh deleted file mode 100644 index c02f2e9..0000000 --- a/kubernetes/deploy.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -poetry version major -export PYPISTATS_VERSION=$(poetry version | tail -c +14) -docker build -t us.gcr.io/pypistats-org/pypistats:${PYPISTATS_VERSION} . -docker push us.gcr.io/pypistats-org/pypistats:${PYPISTATS_VERSION} -kubectl create secret generic pypistats-secrets --from-env-file=gke.env --namespace=pypistats --dry-run -o yaml | kubectl apply -f - -sed -i '.bak' 's|us.gcr.io\/pypistats-org\/pypistats.*|us.gcr.io\/pypistats-org\/pypistats:'"$PYPISTATS_VERSION"'|g' kubernetes/*.yaml -rm kubernetes/*.bak -kubectl apply -f kubernetes/redis.yaml --namespace=pypistats -kubectl apply -f kubernetes/tasks.yaml --namespace=pypistats -kubectl apply -f kubernetes/flower.yaml --namespace=pypistats -kubectl apply -f kubernetes/web.yaml --namespace=pypistats diff --git a/kubernetes/flower.yaml b/kubernetes/flower.yaml deleted file mode 100644 index 729dc9c..0000000 --- a/kubernetes/flower.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: flower - namespace: pypistats - labels: - app: pypistats - component: flower -spec: - replicas: 1 - selector: - matchLabels: - app: pypistats - component: flower - template: - metadata: - labels: - app: pypistats - component: flower - spec: - containers: - - name: pypistats-flower - image: us.gcr.io/pypistats-org/pypistats:11 - imagePullPolicy: Always - args: ["flower"] - envFrom: - - secretRef: - name: pypistats-secrets - ---- - -apiVersion: v1 -kind: Service -metadata: - name: flower - labels: - app: pypistats - component: flower -spec: - ports: - - port: 5555 - targetPort: 5555 - selector: - app: pypistats - component: flower diff --git a/kubernetes/namespace.yaml b/kubernetes/namespace.yaml deleted file mode 100644 index 45ce0a2..0000000 --- a/kubernetes/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: pypistats diff --git a/kubernetes/redis.yaml b/kubernetes/redis.yaml deleted file mode 100644 index c632266..0000000 --- a/kubernetes/redis.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis - labels: - app: redis -spec: - selector: - matchLabels: - app: redis - replicas: 1 - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: redis:5.0.7-alpine - ports: - - containerPort: 6379 - ---- - -apiVersion: v1 -kind: Service -metadata: - name: redis - labels: - app: redis -spec: - ports: - - port: 6379 - targetPort: 6379 - selector: - app: redis diff --git a/kubernetes/tasks.yaml b/kubernetes/tasks.yaml deleted file mode 100644 index 267ed88..0000000 --- a/kubernetes/tasks.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tasks - namespace: pypistats - labels: - app: pypistats - component: tasks -spec: - replicas: 1 - selector: - matchLabels: - app: pypistats - component: tasks - template: - metadata: - labels: - app: pypistats - component: tasks - spec: - containers: - - name: beat - image: us.gcr.io/pypistats-org/pypistats:11 - imagePullPolicy: Always - args: ["beat"] - envFrom: - - secretRef: - name: pypistats-secrets - - name: celery - image: us.gcr.io/pypistats-org/pypistats:11 - imagePullPolicy: Always - args: ["celery"] - envFrom: - - secretRef: - name: pypistats-secrets diff --git a/kubernetes/web.yaml b/kubernetes/web.yaml deleted file mode 100644 index 98eee75..0000000 --- a/kubernetes/web.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: web - namespace: pypistats - labels: - app: pypistats - component: web -spec: - replicas: 2 - selector: - matchLabels: - app: pypistats - component: web - template: - metadata: - labels: - app: pypistats - component: web - spec: - initContainers: - - name: migrate - image: us.gcr.io/pypistats-org/pypistats:11 - imagePullPolicy: Always - envFrom: - - secretRef: - name: pypistats-secrets - args: ["migrate"] - containers: - - name: web - image: us.gcr.io/pypistats-org/pypistats:11 - imagePullPolicy: Always - envFrom: - - secretRef: - name: pypistats-secrets - args: ["web"] - ports: - - containerPort: 5000 - readinessProbe: - httpGet: - path: /health - port: 5000 - initialDelaySeconds: 5 - periodSeconds: 5 - ---- - -apiVersion: v1 -kind: Service -metadata: - name: web - namespace: pypistats -spec: - type: NodePort - ports: - - name: http - protocol: TCP - port: 5000 - targetPort: 5000 - selector: - app: pypistats - component: web - ---- - -apiVersion: networking.k8s.io/v1beta1 -kind: Ingress -metadata: - name: web - namespace: pypistats -spec: - backend: - serviceName: web - servicePort: http - rules: - - http: - paths: - - backend: - serviceName: web - servicePort: http - path: / diff --git a/migrations/README b/migrations/README deleted file mode 100755 index 98e4f9c..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini deleted file mode 100644 index 7884f0f..0000000 --- a/migrations/alembic.ini +++ /dev/null @@ -1,45 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# template used to generate migration files -file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(rev)s_%%(slug)s - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100755 index f3bdb4b..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,88 +0,0 @@ -import logging -from logging.config import fileConfig - -from alembic import context -from flask import current_app -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) -logger = logging.getLogger("alembic.env") - - -config.set_main_option("sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI")) -target_metadata = current_app.extensions["migrate"].db.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure(url=url) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, "autogenerate", False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info("No changes in schema detected.") - - engine = engine_from_config( - config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool - ) - - connection = engine.connect() - context.configure( - connection=connection, - target_metadata=target_metadata, - compare_type=True, - process_revision_directives=process_revision_directives, - **current_app.extensions["migrate"].configure_args, - ) - - try: - with context.begin_transaction(): - context.run_migrations() - finally: - connection.close() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100755 index 2c01563..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/migrations/seeds.py b/migrations/seeds.py deleted file mode 100644 index ea13e1e..0000000 --- a/migrations/seeds.py +++ /dev/null @@ -1,97 +0,0 @@ -import datetime -import logging -import random -import subprocess -import sys - -from pypistats.application import create_app -from pypistats.application import db -from pypistats.models.download import OverallDownloadCount -from pypistats.models.download import PythonMajorDownloadCount -from pypistats.models.download import PythonMinorDownloadCount -from pypistats.models.download import RecentDownloadCount -from pypistats.models.download import SystemDownloadCount - -# required to use the db models outside of the context of the app -app = create_app() -app.app_context().push() - -if db.session.query(RecentDownloadCount.package).count() > 0: - print("Seeds already exist.") - sys.exit(0) - -# use the currently installed dependencies as seed packages -result = subprocess.run(["poetry", "show"], stdout=subprocess.PIPE) -output = result.stdout.decode() - -# extract just the package names from the output -# skip the first line which is a poetry warning -# and the last line which is empty -packages = [] -for line in output.split("\n")[1:-1]: - packages.append(line.split(" ")[0]) - -# add some packages that have optional dependencies -packages.append("apache-airflow") -packages.append("databricks-dbapi") - -logging.info(packages) - -# take the last 120 days -end_date = datetime.date.today() -date_list = [end_date - datetime.timedelta(days=x) for x in range(120)][::-1] - -baseline = 1000 - -# build a bunch of seed records with random values -records = [] -for package in packages + ["__all__"]: - print("Seeding: " + package) - - for idx, category in enumerate(["day", "week", "month"]): - record = RecentDownloadCount( - package=package, category=category, downloads=baseline * (idx + 1) + random.randint(-100, 100) - ) - records.append(record) - - for date in date_list: - - for idx, category in enumerate(["with_mirrors", "without_mirrors"]): - record = OverallDownloadCount( - date=date, - package=package, - category=category, - downloads=baseline * (idx + 1) + random.randint(-100, 100), - ) - records.append(record) - - for idx, category in enumerate(["2", "3"]): - record = PythonMajorDownloadCount( - date=date, - package=package, - category=category, - downloads=baseline * (idx + 1) + random.randint(-100, 100), - ) - records.append(record) - - for idx, category in enumerate(["2.7", "3.4", "3.5", "3.6", "3.7", "3.8"]): - record = PythonMinorDownloadCount( - date=date, - package=package, - category=category, - downloads=baseline * (idx + 1) + random.randint(-100, 100), - ) - records.append(record) - - for idx, category in enumerate(["windows", "linux", "darwin"]): - record = SystemDownloadCount( - date=date, - package=package, - category=category, - downloads=baseline * (idx + 1) + random.randint(-100, 100), - ) - records.append(record) - -# push to the local database -db.session.bulk_save_objects(records) -db.session.commit() diff --git a/migrations/versions/20200303_221751_0cf9945079f1_setup_tables.py b/migrations/versions/20200303_221751_0cf9945079f1_setup_tables.py deleted file mode 100644 index d1f014c..0000000 --- a/migrations/versions/20200303_221751_0cf9945079f1_setup_tables.py +++ /dev/null @@ -1,95 +0,0 @@ -"""setup_tables - -Revision ID: 0cf9945079f1 -Revises: -Create Date: 2020-03-03 22:17:51.438119 - -""" -import sqlalchemy as sa -from alembic import op -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = "0cf9945079f1" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "overall", - sa.Column("date", sa.Date(), nullable=False), - sa.Column("package", sa.String(length=128), nullable=False), - sa.Column("category", sa.String(length=16), nullable=False), - sa.Column("downloads", sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint("date", "package", "category"), - ) - op.create_index(op.f("ix_overall_package"), "overall", ["package"], unique=False) - op.create_table( - "python_major", - sa.Column("date", sa.Date(), nullable=False), - sa.Column("package", sa.String(length=128), nullable=False), - sa.Column("category", sa.String(length=4), nullable=True), - sa.Column("downloads", sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint("date", "package", "category"), - ) - op.create_index(op.f("ix_python_major_package"), "python_major", ["package"], unique=False) - op.create_table( - "python_minor", - sa.Column("date", sa.Date(), nullable=False), - sa.Column("package", sa.String(length=128), nullable=False), - sa.Column("category", sa.String(length=4), nullable=True), - sa.Column("downloads", sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint("date", "package", "category"), - ) - op.create_index(op.f("ix_python_minor_package"), "python_minor", ["package"], unique=False) - op.create_table( - "recent", - sa.Column("package", sa.String(length=128), nullable=False), - sa.Column("category", sa.String(length=8), nullable=False), - sa.Column("downloads", sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint("package", "category"), - ) - op.create_index(op.f("ix_recent_package"), "recent", ["package"], unique=False) - op.create_table( - "system", - sa.Column("date", sa.Date(), nullable=False), - sa.Column("package", sa.String(length=128), nullable=False), - sa.Column("category", sa.String(length=8), nullable=True), - sa.Column("downloads", sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint("date", "package", "category"), - ) - op.create_index(op.f("ix_system_package"), "system", ["package"], unique=False) - op.create_table( - "users", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column("uid", sa.Integer(), nullable=True), - sa.Column("username", sa.String(length=39), nullable=False), - sa.Column("avatar_url", sa.String(length=256), nullable=True), - sa.Column("token", sa.String(length=256), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("active", sa.Boolean(), nullable=True), - sa.Column("is_admin", sa.Boolean(), nullable=True), - sa.Column("favorites", postgresql.ARRAY(sa.String(length=128), dimensions=1), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("uid"), - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("users") - op.drop_index(op.f("ix_system_package"), table_name="system") - op.drop_table("system") - op.drop_index(op.f("ix_recent_package"), table_name="recent") - op.drop_table("recent") - op.drop_index(op.f("ix_python_minor_package"), table_name="python_minor") - op.drop_table("python_minor") - op.drop_index(op.f("ix_python_major_package"), table_name="python_major") - op.drop_table("python_major") - op.drop_index(op.f("ix_overall_package"), table_name="overall") - op.drop_table("overall") - # ### end Alembic commands ### diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa99d15 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "pypistats", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "svelte-kit build", + "start": "node build/index.js", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check .", + "db:studio": "prisma studio", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:deploy": "prisma migrate deploy" + }, + "dependencies": { + "@google-cloud/bigquery": "^8.1.1", + "@prisma/client": "^6.13.0", + "@sveltejs/adapter-node": "^5.2.8", + "@types/node-cron": "^3.0.11", + "node-cron": "^4.2.1", + "redis": "^5.7.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.1", + "@sveltejs/kit": "^2.27.0", + "@sveltejs/vite-plugin-svelte": "^6.1.0", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.11", + "mdsvex": "^0.12.6", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.6.14", + "prisma": "^6.13.0", + "svelte": "^5.37.3", + "svelte-check": "^4.3.1", + "tailwindcss": "^4.1.11", + "typescript": "^5.9.2", + "vite": "^7.0.6" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..39d99d8 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2797 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@google-cloud/bigquery': + specifier: ^8.1.1 + version: 8.1.1 + '@prisma/client': + specifier: ^6.13.0 + version: 6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2) + '@sveltejs/adapter-node': + specifier: ^5.2.8 + version: 5.2.14(@sveltejs/kit@2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1))) + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 + node-cron: + specifier: ^4.2.1 + version: 4.2.1 + redis: + specifier: ^5.7.0 + version: 5.7.0 + devDependencies: + '@sveltejs/adapter-auto': + specifier: ^6.0.1 + version: 6.0.1(@sveltejs/kit@2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1))) + '@sveltejs/kit': + specifier: ^2.27.0 + version: 2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.1.0 + version: 6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + '@tailwindcss/forms': + specifier: ^0.5.10 + version: 0.5.10(tailwindcss@4.1.11) + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@4.1.11) + '@tailwindcss/vite': + specifier: ^4.1.11 + version: 4.1.11(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + mdsvex: + specifier: ^0.12.6 + version: 0.12.6(svelte@5.37.3) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.4.0 + version: 3.4.0(prettier@3.6.2)(svelte@5.37.3) + prettier-plugin-tailwindcss: + specifier: ^0.6.14 + version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.37.3))(prettier@3.6.2) + prisma: + specifier: ^6.13.0 + version: 6.13.0(typescript@5.9.2) + svelte: + specifier: ^5.37.3 + version: 5.37.3 + svelte-check: + specifier: ^4.3.1 + version: 4.3.1(picomatch@4.0.3)(svelte@5.37.3)(typescript@5.9.2) + tailwindcss: + specifier: ^4.1.11 + version: 4.1.11 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + vite: + specifier: ^7.0.6 + version: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@google-cloud/bigquery@8.1.1': + resolution: {integrity: sha512-2GHlohfA/VJffTvibMazMsZi6jPRx8MmaMberyDTL8rnhVs/frKSXVVRtLU83uSAy2j/5SD4mOs4jMQgJPON2g==} + engines: {node: '>=18'} + + '@google-cloud/common@6.0.0': + resolution: {integrity: sha512-IXh04DlkLMxWgYLIUYuHHKXKOUwPDzDgke1ykkkJPe48cGIS9kkL2U/o0pm4ankHLlvzLF/ma1eO86n/bkumIA==} + engines: {node: '>=18'} + + '@google-cloud/paginator@6.0.0': + resolution: {integrity: sha512-g5nmMnzC+94kBxOKkLGpK1ikvolTFCC3s2qtE4F+1EuArcJ7HHC23RDQVt3Ra3CqpUYZ+oXNKZ8n5Cn5yug8DA==} + engines: {node: '>=18'} + + '@google-cloud/precise-date@5.0.0': + resolution: {integrity: sha512-9h0Gvw92EvPdE8AK8AgZPbMnH5ftDyPtKm7/KUfcJVaPEPjwGDsJd1QV0H8esBDV4II41R/2lDWH1epBqIoKUw==} + engines: {node: '>=18'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.1.0': + resolution: {integrity: sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==} + engines: {node: '>=18'} + + '@google-cloud/promisify@5.0.0': + resolution: {integrity: sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==} + engines: {node: '>=18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@prisma/client@6.13.0': + resolution: {integrity: sha512-8m2+I3dQovkV8CkDMluiwEV1TxV9EXdT6xaCz39O6jYw7mkf5gwfmi+cL4LJsEPwz5tG7sreBwkRpEMJedGYUQ==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.13.0': + resolution: {integrity: sha512-OYMM+pcrvj/NqNWCGESSxVG3O7kX6oWuGyvufTUNnDw740KIQvNyA4v0eILgkpuwsKIDU36beZCkUtIt0naTog==} + + '@prisma/debug@6.13.0': + resolution: {integrity: sha512-um+9pfKJW0ihmM83id9FXGi5qEbVJ0Vxi1Gm0xpYsjwUBnw6s2LdPBbrsG9QXRX46K4CLWCTNvskXBup4i9hlw==} + + '@prisma/engines-version@6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd': + resolution: {integrity: sha512-MpPyKSzBX7P/ZY9odp9TSegnS/yH3CSbchQE9f0yBg3l2QyN59I6vGXcoYcqKC9VTniS1s18AMmhyr1OWavjHg==} + + '@prisma/engines@6.13.0': + resolution: {integrity: sha512-D+1B79LFvtWA0KTt8ALekQ6A/glB9w10ETknH5Y9g1k2NYYQOQy93ffiuqLn3Pl6IPJG3EsK/YMROKEaq8KBrA==} + + '@prisma/fetch-engine@6.13.0': + resolution: {integrity: sha512-grmmq+4FeFKmaaytA8Ozc2+Tf3BC8xn/DVJos6LL022mfRlMZYjT3hZM0/xG7+5fO95zFG9CkDUs0m1S2rXs5Q==} + + '@prisma/get-platform@6.13.0': + resolution: {integrity: sha512-Nii2pX50fY4QKKxQwm7/vvqT6Ku8yYJLZAFX4e2vzHwRdMqjugcOG5hOSLjxqoXb0cvOspV70TOhMzrw8kqAnw==} + + '@redis/bloom@5.7.0': + resolution: {integrity: sha512-KtBHDH2Aw1BxYDQd87PJsdEmZcpMbD4oPzdBwB4IvSRmMovukO2NNGi5vpCHhCoicS83zu7cjX1fw79uFBZFJA==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.7.0 + + '@redis/client@5.7.0': + resolution: {integrity: sha512-YV3Knspdj9k6H6s4v8QRcj1WBxHt40vtPmszLKGwRUOUpUOLWSlI9oCUjprMDcQNzgSCXGXYdL/Aj6nT2+Ub0w==} + engines: {node: '>= 18'} + + '@redis/json@5.7.0': + resolution: {integrity: sha512-VP3wtse1PSB/UjZAV1lWyDrWrrZcwi/cjb3L0lIarcIJ+EbHliB2QPml0Bvjz8F8F0eDJRtChJVXFc+jhGxCtA==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.7.0 + + '@redis/search@5.7.0': + resolution: {integrity: sha512-dDZIq8pZJnT+kZ9xRlLLi2Rvkd792z9eh31QRIwPr5wXjAXeaQ+Nf65em6dLpsxZ60MmhwDwLrBPJpYVjKPBPQ==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.7.0 + + '@redis/time-series@5.7.0': + resolution: {integrity: sha512-AJTF9sz3y1MJAukgQW4Jw8zt8qGOE3+1d87pufOP35zsFBlHipGscpctoXiNMebfy0114y/FjSprr65LjbJQSQ==} + engines: {node: '>= 18'} + peerDependencies: + '@redis/client': ^5.7.0 + + '@rollup/plugin-commonjs@28.0.6': + resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.1': + resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.2.0': + resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-auto@6.0.1': + resolution: {integrity: sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/adapter-node@5.2.14': + resolution: {integrity: sha512-TjJvfw0HZlbBGGAW2vFtdGjdKhqpGW3ZDIz0nzy8Zx6Ki6oFmYTjV5Kwn3LWTsyjbsUSXhfFPCuYop3z1iS9qQ==} + peerDependencies: + '@sveltejs/kit': ^2.4.0 + + '@sveltejs/kit@2.27.0': + resolution: {integrity: sha512-pEX1Z2Km8tqmkni+ykIIou+ojp/7gb3M9tpllN5nDWNo9zlI0dI8/hDKFyBwQvb4jYR+EyLriFtrmgJ6GvbnBA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.0': + resolution: {integrity: sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.1.0': + resolution: {integrity: sha512-+U6lz1wvGEG/BvQyL4z/flyNdQ9xDNv5vrh+vWBWTHaebqT0c9RNggpZTo/XSPoHsSCWBlYaTlRX8pZ9GATXCw==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@tailwindcss/forms@0.5.10': + resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + + '@types/node@24.2.0': + resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + arrify@3.0.0: + resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + big.js@6.2.2: + resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + effect@3.16.12: + resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + engines: {node: '>= 0.12'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@7.1.1: + resolution: {integrity: sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==} + engines: {node: '>=18'} + + gcp-metadata@7.0.1: + resolution: {integrity: sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + google-auth-library@10.2.1: + resolution: {integrity: sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A==} + engines: {node: '>=18'} + + google-logging-utils@1.1.1: + resolution: {integrity: sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==} + engines: {node: '>=14'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + index-to-position@1.1.0: + resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdsvex@0.12.6: + resolution: {integrity: sha512-pupx2gzWh3hDtm/iDW4WuCpljmyHbHi34r7ktOqpPGvyiM4MyfNgdJ3qMizXdgCErmvYC9Nn/qyjePy+4ss9Wg==} + peerDependencies: + svelte: ^3.56.0 || ^4.0.0 || ^5.0.0-next.120 + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-cron@4.2.1: + resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} + engines: {node: '>=6.0.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + normalize-package-data@6.0.2: + resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} + engines: {node: ^16.14.0 || >=18.0.0} + + nypm@0.6.1: + resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier-plugin-svelte@3.4.0: + resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier-plugin-tailwindcss@0.6.14: + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + prism-svelte@0.4.7: + resolution: {integrity: sha512-yABh19CYbM24V7aS7TuPYRNMqthxwbvx6FF/Rw920YbyBWO3tnyPIqRMgHuSVsLmuHkkBS1Akyof463FVdkeDQ==} + + prisma@6.13.0: + resolution: {integrity: sha512-dfzORf0AbcEyyzxuv2lEwG8g+WRGF/qDQTpHf/6JoHsyF5MyzCEZwClVaEmw3WXcobgadosOboKUgQU0kFs9kw==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + read-package-up@11.0.0: + resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} + engines: {node: '>=18'} + + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redis@5.7.0: + resolution: {integrity: sha512-ZRbiWYBUYdDTopodRjCVwwCLThrkciPW3bOrkdMCW3nYEelBwUGN6SovmACDsiLUB7mnU3mXnaI5f0W7bDcwng==} + engines: {node: '>= 18'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + retry-request@8.0.0: + resolution: {integrity: sha512-dJkZNmyV9C8WKUmbdj1xcvVlXBSvsUQCkg89TCK8rD72RdSn9A2jlXlS2VuYSTHoPJjJEfUHhjNYrlvuksF9cg==} + engines: {node: '>=18'} + + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-check@4.3.1: + resolution: {integrity: sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte@5.37.3: + resolution: {integrity: sha512-7t/ejshehHd+95z3Z7ebS7wsqHDQxi/8nBTuTRwpMgNegfRBfuitCSKTUDKIBOExqfT2+DhQ2VLG8Xn+cBXoaQ==} + engines: {node: '>=18'} + + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + teeny-request@10.1.0: + resolution: {integrity: sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==} + engines: {node: '>=18'} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + + vite@7.0.6: + resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.27.1': {} + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@google-cloud/bigquery@8.1.1': + dependencies: + '@google-cloud/common': 6.0.0 + '@google-cloud/paginator': 6.0.0 + '@google-cloud/precise-date': 5.0.0 + '@google-cloud/promisify': 5.0.0 + arrify: 3.0.0 + big.js: 6.2.2 + duplexify: 4.1.3 + extend: 3.0.2 + stream-events: 1.0.5 + teeny-request: 10.1.0 + transitivePeerDependencies: + - supports-color + + '@google-cloud/common@6.0.0': + dependencies: + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.1.0 + arrify: 2.0.1 + duplexify: 4.1.3 + extend: 3.0.2 + google-auth-library: 10.2.1 + html-entities: 2.6.0 + retry-request: 8.0.0 + teeny-request: 10.1.0 + transitivePeerDependencies: + - supports-color + + '@google-cloud/paginator@6.0.0': + dependencies: + extend: 3.0.2 + + '@google-cloud/precise-date@5.0.0': {} + + '@google-cloud/projectify@4.0.0': {} + + '@google-cloud/promisify@4.1.0': {} + + '@google-cloud/promisify@5.0.0': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@polka/url@1.0.0-next.29': {} + + '@prisma/client@6.13.0(prisma@6.13.0(typescript@5.9.2))(typescript@5.9.2)': + optionalDependencies: + prisma: 6.13.0(typescript@5.9.2) + typescript: 5.9.2 + + '@prisma/config@6.13.0': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.16.12 + read-package-up: 11.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.13.0': {} + + '@prisma/engines-version@6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd': {} + + '@prisma/engines@6.13.0': + dependencies: + '@prisma/debug': 6.13.0 + '@prisma/engines-version': 6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd + '@prisma/fetch-engine': 6.13.0 + '@prisma/get-platform': 6.13.0 + + '@prisma/fetch-engine@6.13.0': + dependencies: + '@prisma/debug': 6.13.0 + '@prisma/engines-version': 6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd + '@prisma/get-platform': 6.13.0 + + '@prisma/get-platform@6.13.0': + dependencies: + '@prisma/debug': 6.13.0 + + '@redis/bloom@5.7.0(@redis/client@5.7.0)': + dependencies: + '@redis/client': 5.7.0 + + '@redis/client@5.7.0': + dependencies: + cluster-key-slot: 1.1.2 + + '@redis/json@5.7.0(@redis/client@5.7.0)': + dependencies: + '@redis/client': 5.7.0 + + '@redis/search@5.7.0(@redis/client@5.7.0)': + dependencies: + '@redis/client': 5.7.0 + + '@redis/time-series@5.7.0(@redis/client@5.7.0)': + dependencies: + '@redis/client': 5.7.0 + + '@rollup/plugin-commonjs@28.0.6(rollup@4.46.2)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.6(picomatch@4.0.3) + is-reference: 1.2.1 + magic-string: 0.30.17 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.46.2 + + '@rollup/plugin-json@6.1.0(rollup@4.46.2)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + optionalDependencies: + rollup: 4.46.2 + + '@rollup/plugin-node-resolve@16.0.1(rollup@4.46.2)': + dependencies: + '@rollup/pluginutils': 5.2.0(rollup@4.46.2) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + optionalDependencies: + rollup: 4.46.2 + + '@rollup/pluginutils@5.2.0(rollup@4.46.2)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.46.2 + + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true + + '@rollup/rollup-android-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-x64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.2': + optional: true + + '@standard-schema/spec@1.0.0': {} + + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-auto@6.0.1(@sveltejs/kit@2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))': + dependencies: + '@sveltejs/kit': 2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + + '@sveltejs/adapter-node@5.2.14(@sveltejs/kit@2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))': + dependencies: + '@rollup/plugin-commonjs': 28.0.6(rollup@4.46.2) + '@rollup/plugin-json': 6.1.0(rollup@4.46.2) + '@rollup/plugin-node-resolve': 16.0.1(rollup@4.46.2) + '@sveltejs/kit': 2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + rollup: 4.46.2 + + '@sveltejs/kit@2.27.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.1.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.17 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.1 + sirv: 3.0.1 + svelte: 5.37.3 + vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1) + + '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + debug: 4.4.1 + svelte: 5.37.3 + vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.37.3)(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + debug: 4.4.1 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.17 + svelte: 5.37.3 + vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1) + vitefu: 1.1.1(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)) + transitivePeerDependencies: + - supports-color + + '@tailwindcss/forms@0.5.10(tailwindcss@4.1.11)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.1.11 + + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.2 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.11)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.11 + + '@tailwindcss/vite@4.1.11(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1))': + dependencies: + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + tailwindcss: 4.1.11 + vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1) + + '@tootallnate/once@2.0.0': {} + + '@types/caseless@0.12.5': {} + + '@types/cookie@0.6.0': {} + + '@types/estree@1.0.8': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 2.0.11 + + '@types/node-cron@3.0.11': {} + + '@types/node@24.2.0': + dependencies: + undici-types: 7.10.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 24.2.0 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.5 + + '@types/resolve@1.20.2': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/unist@2.0.11': {} + + acorn@8.15.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + aria-query@5.3.2: {} + + arrify@2.0.1: {} + + arrify@3.0.0: {} + + asynckit@0.4.0: {} + + axobject-query@4.1.0: {} + + base64-js@1.5.1: {} + + big.js@6.2.2: {} + + bignumber.js@9.3.1: {} + + buffer-equal-constant-time@1.0.1: {} + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.5.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.2.0 + rc9: 2.1.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commondir@1.0.1: {} + + confbox@0.2.2: {} + + consola@3.4.2: {} + + cookie@0.6.0: {} + + cssesc@3.0.0: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.5: {} + + detect-libc@2.0.4: {} + + devalue@5.1.1: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + effect@3.16.12: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + esm-env@1.2.2: {} + + esrap@2.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + estree-walker@2.0.2: {} + + exsolve@1.0.7: {} + + extend@3.0.2: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + find-up-simple@1.0.1: {} + + form-data@2.5.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gaxios@7.1.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@7.0.1: + dependencies: + gaxios: 7.1.1 + google-logging-utils: 1.1.1 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.1 + pathe: 2.0.3 + + google-auth-library@10.2.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.1 + gcp-metadata: 7.0.1 + google-logging-utils: 1.1.1 + gtoken: 8.0.0 + jws: 4.0.0 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.1: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gtoken@8.0.0: + dependencies: + gaxios: 7.1.1 + jws: 4.0.0 + transitivePeerDependencies: + - supports-color + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + html-entities@2.6.0: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + index-to-position@1.1.0: {} + + inherits@2.0.4: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-module@1.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + jiti@2.5.1: {} + + js-tokens@4.0.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + kleur@4.1.5: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + locate-character@3.0.0: {} + + lodash.castarray@4.4.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + math-intrinsics@1.1.0: {} + + mdsvex@0.12.6(svelte@5.37.3): + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 2.0.11 + prism-svelte: 0.4.7 + prismjs: 1.30.0 + svelte: 5.37.3 + unist-util-visit: 2.0.3 + vfile-message: 2.0.4 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mini-svg-data-uri@1.4.4: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-cron@4.2.1: {} + + node-domexception@1.0.0: {} + + node-fetch-native@1.6.7: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + normalize-package-data@6.0.2: + dependencies: + hosted-git-info: 7.0.2 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + + nypm@0.6.1: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.2.0 + tinyexec: 1.0.1 + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.37.3): + dependencies: + prettier: 3.6.2 + svelte: 5.37.3 + + prettier-plugin-tailwindcss@0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.37.3))(prettier@3.6.2): + dependencies: + prettier: 3.6.2 + optionalDependencies: + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.37.3) + + prettier@3.6.2: {} + + prism-svelte@0.4.7: {} + + prisma@6.13.0(typescript@5.9.2): + dependencies: + '@prisma/config': 6.13.0 + '@prisma/engines': 6.13.0 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - magicast + + prismjs@1.30.0: {} + + pure-rand@6.1.0: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + + read-package-up@11.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 9.0.1 + type-fest: 4.41.0 + + read-pkg@9.0.1: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 8.3.0 + type-fest: 4.41.0 + unicorn-magic: 0.1.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + redis@5.7.0: + dependencies: + '@redis/bloom': 5.7.0(@redis/client@5.7.0) + '@redis/client': 5.7.0 + '@redis/json': 5.7.0(@redis/client@5.7.0) + '@redis/search': 5.7.0(@redis/client@5.7.0) + '@redis/time-series': 5.7.0(@redis/client@5.7.0) + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry-request@8.0.0: + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 10.1.0 + transitivePeerDependencies: + - supports-color + + rollup@4.46.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 + fsevents: 2.3.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-buffer@5.2.1: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stubs@3.0.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-check@4.3.1(picomatch@4.0.3)(svelte@5.37.3)(typescript@5.9.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + chokidar: 4.0.3 + fdir: 6.4.6(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.37.3 + typescript: 5.9.2 + transitivePeerDependencies: + - picomatch + + svelte@5.37.3: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.4 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 2.1.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.17 + zimmerframe: 1.1.2 + + tailwindcss@4.1.11: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + teeny-request@10.1.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 3.3.2 + stream-events: 1.0.5 + transitivePeerDependencies: + - supports-color + + tinyexec@1.0.1: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + totalist@3.0.1: {} + + type-fest@4.41.0: {} + + typescript@5.9.2: {} + + undici-types@7.10.0: {} + + unicorn-magic@0.1.0: {} + + unist-util-is@4.1.0: {} + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + util-deprecate@1.0.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + + vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1): + dependencies: + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.2.0 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + + vitefu@1.1.1(vite@7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1)): + optionalDependencies: + vite: 7.0.6(@types/node@24.2.0)(jiti@2.5.1)(lightningcss@1.30.1) + + web-streams-polyfill@3.3.3: {} + + wrappy@1.0.2: {} + + yallist@5.0.0: {} + + zimmerframe@1.1.2: {} diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index b5ea0c1..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1290 +0,0 @@ -[[package]] -category = "main" -description = "A database migration tool for SQLAlchemy." -name = "alembic" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.2" - -[package.dependencies] -Mako = "*" -SQLAlchemy = ">=1.1.0" -python-dateutil = "*" -python-editor = ">=0.3" - -[[package]] -category = "main" -description = "Low-level AMQP client for Python (fork of amqplib)." -name = "amqp" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.6.1" - -[package.dependencies] -vine = ">=1.1.3,<5.0.0a1" - -[[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" -optional = false -python-versions = "*" -version = "1.4.4" - -[[package]] -category = "dev" -description = "Classes Without Boilerplate" -name = "attrs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" - -[package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] - -[[package]] -category = "main" -description = "Python multiprocessing fork with improvements and bugfixes" -name = "billiard" -optional = false -python-versions = "*" -version = "3.6.3.0" - -[[package]] -category = "dev" -description = "The uncompromising code formatter." -name = "black" -optional = false -python-versions = ">=3.6" -version = "19.10b0" - -[package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" - -[package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - -[[package]] -category = "main" -description = "Extensible memoizing collections and decorators" -name = "cachetools" -optional = false -python-versions = "~=3.5" -version = "4.1.1" - -[[package]] -category = "main" -description = "Distributed Task Queue." -name = "celery" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.4.7" - -[package.dependencies] -billiard = ">=3.6.3.0,<4.0" -kombu = ">=4.6.10,<4.7" -pytz = ">0.0-dev" -vine = "1.3.0" - -[package.extras] -arangodb = ["pyArango (>=1.3.2)"] -auth = ["cryptography"] -azureblockblob = ["azure-storage (0.36.0)", "azure-common (1.1.5)", "azure-storage-common (1.1.0)"] -brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] -cassandra = ["cassandra-driver (<3.21.0)"] -consul = ["python-consul"] -cosmosdbsql = ["pydocumentdb (2.3.2)"] -couchbase = ["couchbase-cffi (<3.0.0)", "couchbase (<3.0.0)"] -couchdb = ["pycouchdb"] -django = ["Django (>=1.11)"] -dynamodb = ["boto3 (>=1.9.178)"] -elasticsearch = ["elasticsearch"] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent"] -librabbitmq = ["librabbitmq (>=1.5.0)"] -lzma = ["backports.lzma"] -memcache = ["pylibmc"] -mongodb = ["pymongo (>=3.3.0)"] -msgpack = ["msgpack"] -pymemcache = ["python-memcached"] -pyro = ["pyro4"] -redis = ["redis (>=3.2.0)"] -riak = ["riak (>=2.0)"] -s3 = ["boto3 (>=1.9.125)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem"] -sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.9.125)", "pycurl (7.43.0.5)"] -tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] -yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard"] - -[[package]] -category = "main" -description = "Python package for providing Mozilla's CA Bundle." -name = "certifi" -optional = false -python-versions = "*" -version = "2020.6.20" - -[[package]] -category = "main" -description = "Foreign Function Interface for Python calling C code." -marker = "python_version >= \"3.5\"" -name = "cffi" -optional = false -python-versions = "*" -version = "1.14.1" - -[package.dependencies] -pycparser = "*" - -[[package]] -category = "main" -description = "Universal encoding detector for Python 2 and 3" -name = "chardet" -optional = false -python-versions = "*" -version = "3.0.4" - -[[package]] -category = "main" -description = "Composable command line interface toolkit" -name = "click" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" - -[[package]] -category = "main" -description = "A simple framework for building complex web applications." -name = "flask" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.1.2" - -[package.dependencies] -Jinja2 = ">=2.10.1" -Werkzeug = ">=0.15" -click = ">=5.1" -itsdangerous = ">=0.24" - -[package.extras] -dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] -docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] -dotenv = ["python-dotenv"] - -[[package]] -category = "main" -description = "Basic and Digest HTTP authentication for Flask routes" -name = "flask-httpauth" -optional = false -python-versions = "*" -version = "4.1.0" - -[package.dependencies] -Flask = "*" - -[[package]] -category = "main" -description = "Rate limiting for flask applications" -name = "flask-limiter" -optional = false -python-versions = "*" -version = "1.3.1" - -[package.dependencies] -Flask = ">=0.8" -limits = "*" -six = ">=1.4.1" - -[[package]] -category = "main" -description = "User session management for Flask" -name = "flask-login" -optional = false -python-versions = "*" -version = "0.4.1" - -[package.dependencies] -Flask = "*" - -[[package]] -category = "main" -description = "SQLAlchemy database migrations for Flask applications using Alembic" -name = "flask-migrate" -optional = false -python-versions = "*" -version = "2.5.3" - -[package.dependencies] -Flask = ">=0.9" -Flask-SQLAlchemy = ">=1.0" -alembic = ">=0.7" - -[[package]] -category = "main" -description = "Adds SQLAlchemy support to your Flask application." -name = "flask-sqlalchemy" -optional = false -python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*" -version = "2.4.4" - -[package.dependencies] -Flask = ">=0.10" -SQLAlchemy = ">=0.8.0" - -[[package]] -category = "main" -description = "Simple integration of Flask and WTForms." -name = "flask-wtf" -optional = false -python-versions = "*" -version = "0.14.3" - -[package.dependencies] -Flask = "*" -WTForms = "*" -itsdangerous = "*" - -[[package]] -category = "main" -description = "Celery Flower" -name = "flower" -optional = false -python-versions = "*" -version = "0.9.5" - -[package.dependencies] -humanize = "*" -prometheus-client = "0.8.0" -pytz = "*" - -[package.dependencies.celery] -python = ">=3.7" -version = ">=4.3.0" - -[package.dependencies.tornado] -python = ">=3.5.2" -version = ">=5.0.0,<7.0.0" - -[[package]] -category = "main" -description = "GitHub extension for Flask microframework" -name = "github-flask" -optional = false -python-versions = "*" -version = "3.2.0" - -[package.dependencies] -Flask = "*" -requests = "*" - -[[package]] -category = "main" -description = "Google API client core library" -name = "google-api-core" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.22.1" - -[package.dependencies] -google-auth = ">=1.19.1,<2.0dev" -googleapis-common-protos = ">=1.6.0,<2.0dev" -protobuf = ">=3.12.0" -pytz = "*" -requests = ">=2.18.0,<3.0.0dev" -setuptools = ">=34.0.0" -six = ">=1.10.0" - -[package.extras] -grpc = ["grpcio (>=1.29.0,<2.0dev)"] -grpcgcp = ["grpcio-gcp (>=0.2.2)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] - -[[package]] -category = "main" -description = "Google Authentication Library" -name = "google-auth" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.20.1" - -[package.dependencies] -cachetools = ">=2.0.0,<5.0" -pyasn1-modules = ">=0.2.1" -setuptools = ">=40.3.0" -six = ">=1.9.0" - -[package.dependencies.rsa] -python = ">=3.5" -version = ">=3.1.4,<5" - -[[package]] -category = "main" -description = "Google BigQuery API client library" -name = "google-cloud-bigquery" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.26.1" - -[package.dependencies] -google-api-core = ">=1.21.0,<2.0dev" -google-cloud-core = ">=1.1.0,<2.0dev" -google-resumable-media = ">=0.5.0,<2.0dev" -six = ">=1.13.0,<2.0.0dev" - -[package.extras] -all = ["google-cloud-bigquery-storage (>=1.0.0,<2.0.0dev)", "grpcio (>=1.8.2,<2.0dev)", "pyarrow (>=0.16.0,<2.0dev)", "pandas (>=0.17.1)", "pyarrow (>=0.4.1,<0.14.0 || >0.14.0)", "tqdm (>=4.0.0,<5.0.0dev)"] -bqstorage = ["google-cloud-bigquery-storage (>=1.0.0,<2.0.0dev)", "grpcio (>=1.8.2,<2.0dev)", "pyarrow (>=0.16.0,<2.0dev)"] -fastparquet = ["fastparquet", "python-snappy", "llvmlite (<=0.31.0)"] -pandas = ["pandas (>=0.17.1)"] -pyarrow = ["pyarrow (>=0.4.1,<0.14.0 || >0.14.0)"] -tqdm = ["tqdm (>=4.0.0,<5.0.0dev)"] - -[[package]] -category = "main" -description = "Google Cloud API client core library" -name = "google-cloud-core" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.4.1" - -[package.dependencies] -google-api-core = ">=1.19.0,<2.0.0dev" - -[package.extras] -grpc = ["grpcio (>=1.8.2,<2.0dev)"] - -[[package]] -category = "main" -description = "A python wrapper of the C library 'Google CRC32C'" -marker = "python_version >= \"3.5\"" -name = "google-crc32c" -optional = false -python-versions = ">=3.5" -version = "0.1.0" - -[package.dependencies] -cffi = ">=1.0.0" - -[package.extras] -testing = ["pytest"] - -[[package]] -category = "main" -description = "Utilities for Google Media Downloads and Resumable Uploads" -name = "google-resumable-media" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "0.7.1" - -[package.dependencies] -six = "*" - -[package.dependencies.google-crc32c] -python = ">=3.5" -version = ">=0.1.0,<0.2dev" - -[package.extras] -requests = ["requests (>=2.18.0,<3.0.0dev)"] - -[[package]] -category = "main" -description = "Common protobufs used in Google APIs" -name = "googleapis-common-protos" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.52.0" - -[package.dependencies] -protobuf = ">=3.6.0" - -[package.extras] -grpc = ["grpcio (>=1.0.0)"] - -[[package]] -category = "main" -description = "WSGI HTTP Server for UNIX" -name = "gunicorn" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "19.10.0" - -[package.extras] -eventlet = ["eventlet (>=0.9.7)"] -gevent = ["gevent (>=0.13)"] -tornado = ["tornado (>=0.2)"] - -[[package]] -category = "main" -description = "Python humanize utilities" -name = "humanize" -optional = false -python-versions = ">=3.5" -version = "2.6.0" - -[package.extras] -tests = ["freezegun", "pytest", "pytest-cov"] - -[[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)" -name = "idna" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" - -[[package]] -category = "main" -description = "Read metadata from Python packages" -marker = "python_version >= \"3.7\" and python_version < \"3.8\" or python_version < \"3.8\"" -name = "importlib-metadata" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] - -[[package]] -category = "dev" -description = "A Python utility / library to sort Python imports." -name = "isort" -optional = false -python-versions = ">=3.6,<4.0" -version = "5.4.1" - -[package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib", "tomlkit (>=0.5.3)"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] - -[[package]] -category = "main" -description = "Various helpers to pass data to untrusted environments and back." -name = "itsdangerous" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.0" - -[[package]] -category = "main" -description = "A very fast and expressive template engine." -name = "jinja2" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.2" - -[package.dependencies] -MarkupSafe = ">=0.23" - -[package.extras] -i18n = ["Babel (>=0.8)"] - -[[package]] -category = "main" -description = "Messaging library for Python." -name = "kombu" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.6.11" - -[package.dependencies] -amqp = ">=2.6.0,<2.7" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.18" - -[package.extras] -azureservicebus = ["azure-servicebus (>=0.21.1)"] -azurestoragequeues = ["azure-storage-queue"] -consul = ["python-consul (>=0.6.0)"] -librabbitmq = ["librabbitmq (>=1.5.2)"] -mongodb = ["pymongo (>=3.3.0)"] -msgpack = ["msgpack"] -pyro = ["pyro4"] -qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=3.3.11)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.4.4)", "pycurl (7.43.0.2)"] -yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=1.3.1)"] - -[[package]] -category = "main" -description = "Rate limiting utilities" -name = "limits" -optional = false -python-versions = "*" -version = "1.5.1" - -[package.dependencies] -six = ">=1.4.1" - -[[package]] -category = "main" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -name = "mako" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.3" - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["babel"] -lingua = ["lingua"] - -[[package]] -category = "main" -description = "Safely add untrusted strings to HTML/XML markup." -name = "markupsafe" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.1" - -[[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." -name = "pathspec" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" - -[[package]] -category = "main" -description = "Python client for the Prometheus monitoring system." -name = "prometheus-client" -optional = false -python-versions = "*" -version = "0.8.0" - -[package.extras] -twisted = ["twisted"] - -[[package]] -category = "main" -description = "Protocol Buffers" -name = "protobuf" -optional = false -python-versions = "*" -version = "3.12.4" - -[package.dependencies] -setuptools = "*" -six = ">=1.9" - -[[package]] -category = "main" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -name = "psycopg2-binary" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.8.5" - -[[package]] -category = "main" -description = "ASN.1 types and codecs" -name = "pyasn1" -optional = false -python-versions = "*" -version = "0.4.8" - -[[package]] -category = "main" -description = "A collection of ASN.1-based protocols modules." -name = "pyasn1-modules" -optional = false -python-versions = "*" -version = "0.2.8" - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.5.0" - -[[package]] -category = "main" -description = "C parser in Python" -marker = "python_version >= \"3.5\"" -name = "pycparser" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.20" - -[[package]] -category = "main" -description = "Extensions to the standard Python datetime module" -name = "python-dateutil" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -version = "2.8.1" - -[package.dependencies] -six = ">=1.5" - -[[package]] -category = "main" -description = "Programmatically open an editor, capture the result." -name = "python-editor" -optional = false -python-versions = "*" -version = "1.0.4" - -[[package]] -category = "main" -description = "World timezone definitions, modern and historical" -name = "pytz" -optional = false -python-versions = "*" -version = "2020.1" - -[[package]] -category = "main" -description = "Python client for Redis key-value store" -name = "redis" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "3.5.3" - -[package.extras] -hiredis = ["hiredis (>=0.1.3)"] - -[[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." -name = "regex" -optional = false -python-versions = "*" -version = "2020.7.14" - -[[package]] -category = "main" -description = "Python HTTP for Humans." -name = "requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.24.0" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" - -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] - -[[package]] -category = "main" -description = "Pure-Python RSA implementation" -marker = "python_version >= \"3.5\"" -name = "rsa" -optional = false -python-versions = ">=3.5, <4" -version = "4.6" - -[package.dependencies] -pyasn1 = ">=0.1.3" - -[[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" -name = "six" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" - -[[package]] -category = "main" -description = "Database Abstraction Library" -name = "sqlalchemy" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.18" - -[package.extras] -mssql = ["pyodbc"] -mssql_pymssql = ["pymssql"] -mssql_pyodbc = ["pyodbc"] -mysql = ["mysqlclient"] -oracle = ["cx-oracle"] -postgresql = ["psycopg2"] -postgresql_pg8000 = ["pg8000"] -postgresql_psycopg2binary = ["psycopg2-binary"] -postgresql_psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql"] - -[[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" -optional = false -python-versions = "*" -version = "0.10.1" - -[[package]] -category = "main" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -marker = "python_version >= \"3.5.2\"" -name = "tornado" -optional = false -python-versions = ">= 3.5" -version = "6.0.4" - -[[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" -name = "typed-ast" -optional = false -python-versions = "*" -version = "1.4.1" - -[[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." -name = "urllib3" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.10" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] - -[[package]] -category = "main" -description = "Promises, promises, promises." -name = "vine" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.0" - -[[package]] -category = "main" -description = "The comprehensive WSGI web application library." -name = "werkzeug" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.0.1" - -[package.extras] -dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] -watchdog = ["watchdog"] - -[[package]] -category = "main" -description = "A flexible forms validation and rendering library for Python web development." -name = "wtforms" -optional = false -python-versions = "*" -version = "2.3.3" - -[package.dependencies] -MarkupSafe = "*" - -[package.extras] -email = ["email-validator"] -ipaddress = ["ipaddress"] -locale = ["Babel (>=1.3)"] - -[[package]] -category = "main" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version >= \"3.7\" and python_version < \"3.8\" or python_version < \"3.8\" or python_version >= \"3.7\" and python_version < \"3.8\" and (python_version >= \"3.7\" and python_version < \"3.8\" or python_version < \"3.8\")" -name = "zipp" -optional = false -python-versions = ">=3.6" -version = "3.1.0" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] - -[metadata] -content-hash = "a09394417c83737dee35786b4db40e4629877a2043fe537b3742c5cf0e44717b" -lock-version = "1.0" -python-versions = "^3.7" - -[metadata.files] -alembic = [ - {file = "alembic-1.4.2.tar.gz", hash = "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf"}, -] -amqp = [ - {file = "amqp-2.6.1-py2.py3-none-any.whl", hash = "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"}, - {file = "amqp-2.6.1.tar.gz", hash = "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21"}, -] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, -] -billiard = [ - {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"}, - {file = "billiard-3.6.3.0.tar.gz", hash = "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"}, -] -black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, -] -cachetools = [ - {file = "cachetools-4.1.1-py3-none-any.whl", hash = "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98"}, - {file = "cachetools-4.1.1.tar.gz", hash = "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20"}, -] -celery = [ - {file = "celery-4.4.7-py2.py3-none-any.whl", hash = "sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45"}, - {file = "celery-4.4.7.tar.gz", hash = "sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f"}, -] -certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, -] -cffi = [ - {file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"}, - {file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"}, - {file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"}, - {file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"}, - {file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"}, - {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"}, - {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"}, - {file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"}, - {file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"}, - {file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"}, - {file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"}, - {file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"}, - {file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"}, - {file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"}, - {file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"}, - {file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"}, - {file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"}, - {file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"}, - {file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"}, - {file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"}, - {file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"}, - {file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"}, - {file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"}, - {file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"}, - {file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"}, - {file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"}, - {file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"}, - {file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"}, -] -chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] -click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -flask = [ - {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, - {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, -] -flask-httpauth = [ - {file = "Flask-HTTPAuth-4.1.0.tar.gz", hash = "sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000"}, - {file = "Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl", hash = "sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7"}, -] -flask-limiter = [ - {file = "Flask-Limiter-1.3.1.tar.gz", hash = "sha256:08d6d7534a847c532fd36d0df978f93908d8616813085941c862bbcfcf6811aa"}, - {file = "Flask_Limiter-1.3.1-py3-none-any.whl", hash = "sha256:82c154bd96c0a9c8307d9500876e72b247a35a5c7374a2c606fc32b647a6d8cb"}, - {file = "Flask_Limiter-1.3.1-py3.7.egg", hash = "sha256:856de3b6bd5c43ebc15afd57fc7d05afa8e25610593633ba08e69129c927c511"}, -] -flask-login = [ - {file = "Flask-Login-0.4.1.tar.gz", hash = "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"}, -] -flask-migrate = [ - {file = "Flask-Migrate-2.5.3.tar.gz", hash = "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee"}, - {file = "Flask_Migrate-2.5.3-py2.py3-none-any.whl", hash = "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732"}, -] -flask-sqlalchemy = [ - {file = "Flask-SQLAlchemy-2.4.4.tar.gz", hash = "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"}, - {file = "Flask_SQLAlchemy-2.4.4-py2.py3-none-any.whl", hash = "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e"}, -] -flask-wtf = [ - {file = "Flask-WTF-0.14.3.tar.gz", hash = "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"}, - {file = "Flask_WTF-0.14.3-py2.py3-none-any.whl", hash = "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2"}, -] -flower = [ - {file = "flower-0.9.5-py2.py3-none-any.whl", hash = "sha256:71be02bff7b2f56b0a07bd947fb3c748acba7f44f80ae88125d8954ce1a89697"}, - {file = "flower-0.9.5.tar.gz", hash = "sha256:56916d1d2892e25453d6023437427fc04706a1308e0bd4822321da34e1643f9c"}, -] -github-flask = [ - {file = "GitHub-Flask-3.2.0.tar.gz", hash = "sha256:24600b720f698bac10667b76b136995ba7821d884e58b27e2a18ca0e4760c786"}, -] -google-api-core = [ - {file = "google-api-core-1.22.1.tar.gz", hash = "sha256:35cba563034d668ae90ffe1f03193a84e745b38f09592f60258358b5e5ee6238"}, - {file = "google_api_core-1.22.1-py2.py3-none-any.whl", hash = "sha256:431839101b7edc7b0e6cccca0441cb9015f728fc5f098e146e123bf523e8cf71"}, -] -google-auth = [ - {file = "google-auth-1.20.1.tar.gz", hash = "sha256:2f34dd810090d0d4c9d5787c4ad7b4413d1fbfb941e13682c7a2298d3b6cdcc8"}, - {file = "google_auth-1.20.1-py2.py3-none-any.whl", hash = "sha256:ce1fb80b5c6d3dd038babcc43e221edeafefc72d983b3dc28b67b996f76f00b9"}, -] -google-cloud-bigquery = [ - {file = "google-cloud-bigquery-1.26.1.tar.gz", hash = "sha256:51c29b95d460486d9e0210f63e8193691cd08480b69775270e84dd3db87c1bf2"}, - {file = "google_cloud_bigquery-1.26.1-py2.py3-none-any.whl", hash = "sha256:3cf0acb2ae4f1e6628ad0d02b7bf26c3dcf449e864d2a569372230cdc4d29b82"}, -] -google-cloud-core = [ - {file = "google-cloud-core-1.4.1.tar.gz", hash = "sha256:613e56f164b6bee487dd34f606083a0130f66f42f7b10f99730afdf1630df507"}, - {file = "google_cloud_core-1.4.1-py2.py3-none-any.whl", hash = "sha256:4c9e457fcfc026fdde2e492228f04417d4c717fb0f29f070122fb0ab89e34ebd"}, -] -google-crc32c = [ - {file = "google-crc32c-0.1.0.tar.gz", hash = "sha256:ad3d9b4402d4a16673aba7e74feacd621678aef3a9e6c0a5fb4c7e133c39ac45"}, - {file = "google_crc32c-0.1.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:dab8d637d1467e8dd8e01f8d909c2b92102d9bf4a0e5bc4898c9c1aaccf52572"}, - {file = "google_crc32c-0.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:bce91c6fd8d32ea76c6162cbb7ed493939c85b8c0da41f194f9a7784e978dd91"}, - {file = "google_crc32c-0.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:8dec850f4a4afdc8721b675c549f127c7809d6e76afebb14b1acc58f456d1e10"}, - {file = "google_crc32c-0.1.0-cp35-cp35m-win32.whl", hash = "sha256:7232f2b5305f44fa5bfe01b094305cfab1ab1895091aebcc840f262ef8013271"}, - {file = "google_crc32c-0.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3b0f8b73a97be981a5b727526eb8087a8a33a103031e2ec799df66f7535a152e"}, - {file = "google_crc32c-0.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7496f57ba73f63ea0b36bdab961799d03f1e5d3b972ec00b93a3c13f94bf703a"}, - {file = "google_crc32c-0.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b5806e9f3602e9ab237306700ace5121c8fc7f5cc5a59054255d874123144914"}, - {file = "google_crc32c-0.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f42f3df1ac90326c1229ffb71471f1d1504f8c68fad6b627c996df732e800c6c"}, - {file = "google_crc32c-0.1.0-cp36-cp36m-win32.whl", hash = "sha256:4c8b6ea0fa71913b0e773b311001b390110d466f0c6536bf6bad2b712d11acf5"}, - {file = "google_crc32c-0.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:af1f4ef7c649ad637e7fdd1e6e8e5a1ef28b45325064f9c8b563fe7ef8444e4c"}, - {file = "google_crc32c-0.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb34587503cd052495df010474cf7ff408a43efc56360b1cc7563d1a849d4798"}, - {file = "google_crc32c-0.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:79bf4d11867b3adcb8110b1fafc7d8ca7cb8ee1cd1d65ceaffef5c945188b5b8"}, - {file = "google_crc32c-0.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:053abd3fed09a766a56129b2df1439cf691ac40443659e252cc2cf4ba440c0aa"}, - {file = "google_crc32c-0.1.0-cp37-cp37m-win32.whl", hash = "sha256:2e666e8cdd067ece9e7e2618634caa3aa33266da5c3e9666dd46e5d3e65b3538"}, - {file = "google_crc32c-0.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c9e222263c9028dca294611af0e51371afcfc9bc4781484909d50c6ca9862807"}, - {file = "google_crc32c-0.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcc75bc9ef5a0ba3989408a227f4e6b609e989427727f4bca3aaad1f2ba4c98d"}, - {file = "google_crc32c-0.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:57893cf04cfa924c4e75e9ba29c9878304687bd776f15fb02b6ecdb867d181a3"}, -] -google-resumable-media = [ - {file = "google-resumable-media-0.7.1.tar.gz", hash = "sha256:57841f5e65fb285c01071f439724745b2549a72eb75e5fd979198eb518608ed0"}, - {file = "google_resumable_media-0.7.1-py2.py3-none-any.whl", hash = "sha256:0572998cc2c7ba9ca996337896a2f93dbe8bc88866ebd81c8b7f4d7b07222957"}, -] -googleapis-common-protos = [ - {file = "googleapis-common-protos-1.52.0.tar.gz", hash = "sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351"}, - {file = "googleapis_common_protos-1.52.0-py2.py3-none-any.whl", hash = "sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24"}, -] -gunicorn = [ - {file = "gunicorn-19.10.0-py2.py3-none-any.whl", hash = "sha256:c3930fe8de6778ab5ea716cab432ae6335fa9f03b3f2c3e02529214c476f4bcb"}, - {file = "gunicorn-19.10.0.tar.gz", hash = "sha256:f9de24e358b841567063629cd0a656b26792a41e23a24d0dcb40224fc3940081"}, -] -humanize = [ - {file = "humanize-2.6.0-py3-none-any.whl", hash = "sha256:fd5b32945687443d5b8bc1e02fad027da1d293a9e963b3450122ad98ef534f21"}, - {file = "humanize-2.6.0.tar.gz", hash = "sha256:8ee358ea6c23de896b9d1925ebe6a8504bb2ba7e98d5ccf4d07ab7f3b28f3819"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, -] -isort = [ - {file = "isort-5.4.1-py3-none-any.whl", hash = "sha256:819fa99f2a9323025ade768e94b2e27447cd9b0a64595a90f21595e11151b788"}, - {file = "isort-5.4.1.tar.gz", hash = "sha256:a4fb5fffc46494cda15e33a996c8e724f8e3db19682b84cc7c990b57f2941e9f"}, -] -itsdangerous = [ - {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, - {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, -] -jinja2 = [ - {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, - {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, -] -kombu = [ - {file = "kombu-4.6.11-py2.py3-none-any.whl", hash = "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a"}, - {file = "kombu-4.6.11.tar.gz", hash = "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"}, -] -limits = [ - {file = "limits-1.5.1-py2-none-any.whl", hash = "sha256:0e5f8b10f18dd809eb2342f5046eb9aa5e4e69a0258567b5f4aa270647d438b3"}, - {file = "limits-1.5.1.tar.gz", hash = "sha256:f0c3319f032c4bfad68438ed1325c0fac86dac64582c7c25cddc87a0b658fa20"}, -] -mako = [ - {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, - {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, -] -markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, -] -pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, -] -prometheus-client = [ - {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, - {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, -] -protobuf = [ - {file = "protobuf-3.12.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3d59825cba9447e8f4fcacc1f3c892cafd28b964e152629b3f420a2fb5918b5a"}, - {file = "protobuf-3.12.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6009f3ebe761fad319b52199a49f1efa7a3729302947a78a3f5ea8e7e89e3ac2"}, - {file = "protobuf-3.12.4-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e2bd5c98952db3f1bb1af2e81b6a208909d3b8a2d32f7525c5cc10a6338b6593"}, - {file = "protobuf-3.12.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2becd0e238ae34caf96fa7365b87f65b88aebcf7864dfe5ab461c5005f4256d9"}, - {file = "protobuf-3.12.4-cp35-cp35m-win32.whl", hash = "sha256:ef991cbe34d7bb935ba6349406a210d3558b9379c21621c6ed7b99112af7350e"}, - {file = "protobuf-3.12.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a7b6cf201e67132ca99b8a6c4812fab541fdce1ceb54bb6f66bc336ab7259138"}, - {file = "protobuf-3.12.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4794a7748ee645d2ae305f3f4f0abd459e789c973b5bc338008960f83e0c554b"}, - {file = "protobuf-3.12.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f1796e0eb911bf5b08e76b753953effbeb6bc42c95c16597177f627eaa52c375"}, - {file = "protobuf-3.12.4-cp36-cp36m-win32.whl", hash = "sha256:c0c8d7c8f07eacd9e98a907941b56e57883cf83de069cfaeaa7e02c582f72ddb"}, - {file = "protobuf-3.12.4-cp36-cp36m-win_amd64.whl", hash = "sha256:2db6940c1914fa3fbfabc0e7c8193d9e18b01dbb4650acac249b113be3ba8d9e"}, - {file = "protobuf-3.12.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6842284bb15f1b19c50c5fd496f1e2a4cfefdbdfa5d25c02620cb82793295a7"}, - {file = "protobuf-3.12.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0b00429b87821f1e6f3d641327864e6f271763ae61799f7540bc58a352825fe2"}, - {file = "protobuf-3.12.4-cp37-cp37m-win32.whl", hash = "sha256:f10ba89f9cd508dc00e469918552925ef7cba38d101ca47af1e78f2f9982c6b3"}, - {file = "protobuf-3.12.4-cp37-cp37m-win_amd64.whl", hash = "sha256:2636c689a6a2441da9a2ef922a21f9b8bfd5dfe676abd77d788db4b36ea86bee"}, - {file = "protobuf-3.12.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50b7bb2124f6a1fb0ddc6a44428ae3a21e619ad2cdf08130ac6c00534998ef07"}, - {file = "protobuf-3.12.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e77ca4e1403b363a88bde9e31c11d093565e925e1685f40b29385a52f2320794"}, - {file = "protobuf-3.12.4-py2.py3-none-any.whl", hash = "sha256:32f0bcdf85e0040f36b4f548c71177027f2a618cab00ba235197fa9e230b7289"}, - {file = "protobuf-3.12.4.tar.gz", hash = "sha256:c99e5aea75b6f2b29c8d8da5bdc5f5ed8d9a5b4f15115c8316a3f0a850f94656"}, -] -psycopg2-binary = [ - {file = "psycopg2-binary-2.8.5.tar.gz", hash = "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38"}, - {file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389"}, - {file = "psycopg2_binary-2.8.5-cp34-cp34m-win32.whl", hash = "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9"}, - {file = "psycopg2_binary-2.8.5-cp34-cp34m-win_amd64.whl", hash = "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04"}, - {file = "psycopg2_binary-2.8.5-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3"}, - {file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057"}, - {file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce"}, - {file = "psycopg2_binary-2.8.5-cp35-cp35m-win32.whl", hash = "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4"}, - {file = "psycopg2_binary-2.8.5-cp35-cp35m-win_amd64.whl", hash = "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb"}, - {file = "psycopg2_binary-2.8.5-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434"}, - {file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98"}, - {file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d"}, - {file = "psycopg2_binary-2.8.5-cp36-cp36m-win32.whl", hash = "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1"}, - {file = "psycopg2_binary-2.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162"}, - {file = "psycopg2_binary-2.8.5-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4"}, - {file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab"}, - {file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505"}, - {file = "psycopg2_binary-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3"}, - {file = "psycopg2_binary-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e"}, - {file = "psycopg2_binary-2.8.5-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a"}, - {file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266"}, - {file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522"}, - {file = "psycopg2_binary-2.8.5-cp38-cp38-win32.whl", hash = "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa"}, - {file = "psycopg2_binary-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"}, -] -pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, -] -pyasn1-modules = [ - {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, - {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, - {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, - {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, - {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, - {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, - {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, - {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, - {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, - {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, - {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, - {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, - {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, -] -pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, -] -python-editor = [ - {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, - {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, - {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, - {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, - {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, -] -pytz = [ - {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, - {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, -] -redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, -] -regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, -] -requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, -] -rsa = [ - {file = "rsa-4.6-py3-none-any.whl", hash = "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"}, - {file = "rsa-4.6.tar.gz", hash = "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -sqlalchemy = [ - {file = "SQLAlchemy-1.3.18-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-win32.whl", hash = "sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-win_amd64.whl", hash = "sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-win32.whl", hash = "sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-win_amd64.whl", hash = "sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-win32.whl", hash = "sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-win_amd64.whl", hash = "sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-win32.whl", hash = "sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-win_amd64.whl", hash = "sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-win32.whl", hash = "sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl", hash = "sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8"}, - {file = "SQLAlchemy-1.3.18.tar.gz", hash = "sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7"}, -] -toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, -] -tornado = [ - {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, - {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, - {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, - {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, - {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, - {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, - {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, - {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, - {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -urllib3 = [ - {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, - {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, -] -vine = [ - {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"}, - {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, -] -werkzeug = [ - {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, - {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, -] -wtforms = [ - {file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"}, - {file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"}, -] -zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, -] diff --git a/prisma/migrations/20250804175350_init/migration.sql b/prisma/migrations/20250804175350_init/migration.sql new file mode 100644 index 0000000..03987f0 --- /dev/null +++ b/prisma/migrations/20250804175350_init/migration.sql @@ -0,0 +1,48 @@ +-- CreateTable +CREATE TABLE "public"."recent" ( + "package" TEXT NOT NULL, + "category" TEXT NOT NULL, + "downloads" BIGINT NOT NULL, + + CONSTRAINT "recent_pkey" PRIMARY KEY ("package","category") +); + +-- CreateTable +CREATE TABLE "public"."overall" ( + "date" DATE NOT NULL, + "package" TEXT NOT NULL, + "category" TEXT NOT NULL, + "downloads" INTEGER NOT NULL, + + CONSTRAINT "overall_pkey" PRIMARY KEY ("date","package","category") +); + +-- CreateTable +CREATE TABLE "public"."python_major" ( + "date" DATE NOT NULL, + "package" TEXT NOT NULL, + "category" TEXT, + "downloads" INTEGER NOT NULL, + + CONSTRAINT "python_major_pkey" PRIMARY KEY ("date","package") +); + +-- CreateTable +CREATE TABLE "public"."python_minor" ( + "date" DATE NOT NULL, + "package" TEXT NOT NULL, + "category" TEXT, + "downloads" INTEGER NOT NULL, + + CONSTRAINT "python_minor_pkey" PRIMARY KEY ("date","package") +); + +-- CreateTable +CREATE TABLE "public"."system" ( + "date" DATE NOT NULL, + "package" TEXT NOT NULL, + "category" TEXT, + "downloads" INTEGER NOT NULL, + + CONSTRAINT "system_pkey" PRIMARY KEY ("date","package") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..2278f3e --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,70 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model RecentDownloadCount { + package String + category String + downloads BigInt + + @@id([package, category]) + @@map("recent") +} + +model OverallDownloadCount { + date DateTime @db.Date + package String + category String + downloads Int + + @@id([date, package, category]) + @@map("overall") +} + +model PythonMajorDownloadCount { + date DateTime @db.Date + package String + category String + downloads Int + + @@id([date, package, category]) + @@map("python_major") +} + +model PythonMinorDownloadCount { + date DateTime @db.Date + package String + category String + downloads Int + + @@id([date, package, category]) + @@map("python_minor") +} + +model SystemDownloadCount { + date DateTime @db.Date + package String + category String + downloads Int + + @@id([date, package, category]) + @@map("system") +} + +model InstallerDownloadCount { + date DateTime @db.Date + package String + category String + downloads Int + + @@id([date, package, category]) + @@map("installer") +} diff --git a/pypistats/application.py b/pypistats/application.py deleted file mode 100644 index 830a5a4..0000000 --- a/pypistats/application.py +++ /dev/null @@ -1,50 +0,0 @@ -"""PyPIStats application.""" -from celery import Task -from flask import Flask - -from pypistats import views -from pypistats.config import DevConfig -from pypistats.extensions import celery -from pypistats.extensions import db -from pypistats.extensions import github -from pypistats.extensions import migrate - - -def create_app(config_object=DevConfig): - """Create the application.""" - app = Flask(__name__.split(".")[0]) - app.config.from_object(config_object) - register_extensions(app) - register_blueprints(app) - init_celery(celery, app) - return app - - -def init_celery(celery_, app): - """Create a celery object.""" - celery_.conf.update(app.config) - - class ContextTask(Task): - abstract = True - - def __call__(self, *args, **kwargs): - with app.app_context(): - return Task.__call__(self, *args, **kwargs) - - celery_.Task = ContextTask - - -def register_blueprints(app): - """Register Flask blueprints.""" - app.register_blueprint(views.admin.blueprint) - app.register_blueprint(views.api.blueprint) - app.register_blueprint(views.error.blueprint) - app.register_blueprint(views.general.blueprint) - app.register_blueprint(views.user.blueprint) - - -def register_extensions(app): - """Register Flask extensions.""" - db.init_app(app) - github.init_app(app) - migrate.init_app(app, db) diff --git a/pypistats/config.py b/pypistats/config.py deleted file mode 100644 index a6e0da4..0000000 --- a/pypistats/config.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Application configuration.""" -import os - -from celery.schedules import crontab -from flask import json - - -def get_db_uri(): - """Get the database URI.""" - return "postgresql://{username}:{password}@{host}:{port}/{dbname}".format( - username=os.environ.get("POSTGRESQL_USERNAME"), - password=os.environ.get("POSTGRESQL_PASSWORD"), - host=os.environ.get("POSTGRESQL_HOST"), - port=os.environ.get("POSTGRESQL_PORT"), - dbname=os.environ.get("POSTGRESQL_DBNAME"), - ) - - -class Config: - """Base configuration.""" - - APP_DIR = os.path.abspath(os.path.dirname(__file__)) - CELERY_BROKER_URL = (os.environ.get("CELERY_BROKER_URL"),) - BROKER_TRANSPORT_OPTIONS = {"visibility_timeout": 86400} - CELERY_IMPORTS = "pypistats.tasks.pypi" - CELERYBEAT_SCHEDULE = { - "update_db": {"task": "pypistats.tasks.pypi.etl", "schedule": crontab(minute=0, hour=1)} # 1am UTC - } - GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID") - GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET") - PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - SECRET_KEY = os.environ.get("PYPISTATS_SECRET", "secret-key") - SQLALCHEMY_TRACK_MODIFICATIONS = False - SQLALCHEMY_DATABASE_URI = get_db_uri() - - # Plotly chart definitions - PLOT_BASE = json.load(open(os.path.join(os.path.dirname(__file__), "plots", "plot_base.json"))) - DATA_BASE = json.load(open(os.path.join(os.path.dirname(__file__), "plots", "data_base.json"))) - - -class LocalConfig(Config): - """Local configuration.""" - - DEBUG = True - ENV = "local" - - -class ProdConfig(Config): - """Production configuration.""" - - DEBUG = False - ENV = "prod" - - -class DevConfig(Config): - """Development configuration.""" - - DEBUG = True - ENV = "dev" - - -class TestConfig(Config): - """Test configuration.""" - - DEBUG = True - ENV = "dev" - TESTING = True - WTF_CSRF_ENABLED = False # Allows form testing - - -configs = {"development": DevConfig, "local": LocalConfig, "production": ProdConfig, "test": TestConfig} diff --git a/pypistats/database.py b/pypistats/database.py deleted file mode 100644 index 243016d..0000000 --- a/pypistats/database.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Database classes and models.""" -from pypistats.extensions import db - -Column = db.Column -basestring = (str, bytes) - - -class CRUDMixin(object): - """Mixin that adds convenience methods for CRUD operations.""" - - @classmethod - def create(cls, **kwargs): - """Create a new record and save it the database.""" - instance = cls(**kwargs) - return instance.save() - - def update(self, commit=True, **kwargs): - """Update specific fields of a record.""" - for attr, value in kwargs.items(): - setattr(self, attr, value) - return commit and self.save() or self - - def save(self, commit=True): - """Save the record.""" - db.session.add(self) - if commit: - db.session.commit() - return self - - def delete(self, commit=True): - """Remove the record from the database.""" - db.session.delete(self) - return commit and db.session.commit() - - -class Model(CRUDMixin, db.Model): - """Base model class that includes CRUD convenience methods.""" - - __abstract__ = True - - -class SurrogatePK(object): - """A mixin that adds a surrogate integer "primary key" column. - - Adds a surrogate integer "primary key" column named ``id`` to any - declarative-mapped class. - """ - - __table_args__ = {"extend_existing": True} - - id = Column(db.Integer, primary_key=True) - - @classmethod - def get_by_id(cls, record_id): - """Get record by ID.""" - if any((isinstance(record_id, basestring) and record_id.isdigit(), isinstance(record_id, (int, float)))): - return cls.query.get(int(record_id)) - return None diff --git a/pypistats/extensions.py b/pypistats/extensions.py deleted file mode 100644 index 096ea3d..0000000 --- a/pypistats/extensions.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Flask extensions.""" -from celery import Celery -from flask_github import GitHub -from flask_httpauth import HTTPBasicAuth -from flask_migrate import Migrate -from flask_sqlalchemy import SQLAlchemy - -from pypistats.config import Config - -db = SQLAlchemy() -github = GitHub() -migrate = Migrate() -auth = HTTPBasicAuth() - - -def create_celery(name=__name__, config=Config): - """Create a celery object.""" - redis_uri = "redis://localhost:6379" - celery = Celery(name, broker=redis_uri) - celery.config_from_object(config) - return celery - - -celery = create_celery() diff --git a/pypistats/models/__init__.py b/pypistats/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pypistats/models/download.py b/pypistats/models/download.py deleted file mode 100644 index 54a9f43..0000000 --- a/pypistats/models/download.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Package stats tables.""" -from pypistats.database import Column -from pypistats.database import Model -from pypistats.extensions import db - - -class OverallDownloadCount(Model): - """Overall download counts.""" - - __tablename__ = "overall" - - date = Column(db.Date, primary_key=True, nullable=False) - package = Column(db.String(128), primary_key=True, nullable=False, index=True) - # with_mirrors or without_mirrors - category = Column(db.String(16), primary_key=True, nullable=False) - downloads = Column(db.Integer(), nullable=False) - - def __repr__(self): - return "".format(f"{str(self.package)} - {str(self.category)}") - - -class SystemDownloadCount(Model): - """Download counts by system.""" - - __tablename__ = "system" - - date = Column(db.Date, primary_key=True) - package = Column(db.String(128), primary_key=True, nullable=False, index=True) - # system, e.g. Windows or Linux or Darwin (or null) - category = Column(db.String(8), primary_key=True, nullable=True) - downloads = Column(db.Integer(), nullable=False) - - def __repr__(self): - return "" diff --git a/pypistats/plots/data_base.json b/pypistats/plots/data_base.json deleted file mode 100644 index 69f6f12..0000000 --- a/pypistats/plots/data_base.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "downloads": { - "data": [ - { - "x": [ - "2017-05-01", - "2017-05-02", - "2017-05-03" - ], - "y": [ - "2", - "5", - "4" - ], - "name": "Downloads", - "type": "scatter", - "mode": "lines+markers", - "connectgaps": true, - "marker": { - "symbol": "circle", - "line": { - "color": "#444", - "width": 1 - } - }, - "line": { - "shape": "linear", - "smoothing": 1, - "width": 2 - } - } - ] - }, - "percentages": { - "data": [ - { - "x": [ - "2017-05-01", - "2017-05-02", - "2017-05-03" - ], - "y": [ - "2", - "5", - "4" - ], - "text": [ - "2", - "5", - "4" - ], - "name": "Proportional downloads", - "hoverinfo": "x+text+name", - "type": "scatter", - "mode": "lines+markers", - "connectgaps": true, - "marker": { - "symbol": "circle", - "line": { - "color": "#444", - "width": 1 - } - }, - "line": { - "shape": "linear", - "smoothing": 1, - "width": 2 - } - } - ] - } -} diff --git a/pypistats/plots/plot_base.json b/pypistats/plots/plot_base.json deleted file mode 100644 index 1103075..0000000 --- a/pypistats/plots/plot_base.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "downloads": { - "layout": { - "autosize": true, - "height": 400, - "margin": { - "r": 100, - "t": 80, - "autoexpand": true, - "b": 80, - "l": 100, - "pad": 0 - }, - "paper_bgcolor": "#fff", - "plot_bgcolor": "rgba(175, 175, 175, 0.2)", - "showlegend": true, - "legend": { - "orientation": "v", - "bgcolor": "#e7e7e7", - "xanchor": "left", - "yanchor": "middle", - "x": 0, - "y": 0.5 - }, - "title": "Downloads", - "xaxis": { - "tickformat": "%m-%d", - "dtick": 604800000, - "tick0": "2017-08-07", - "gridcolor": "#FFF", - "gridwidth": 2, - "anchor": "y", - "domain": [ - 0, - 1 - ], - "title": "Date", - "titlefont": { - "family": "Geneva, Verdana, Geneva, sans-serif", - "size": 16, - "color": "#7f7f7f" - }, - "showline": true, - "linecolor": "rgba(148, 148, 148, 1)", - "linewidth": 2, - "tickangle": -45, - "rangeselector": { - "buttons": [ - { - "step": "day", - "stepmode": "backward", - "count": 31, - "label": "30d" - }, - { - "step": "day", - "stepmode": "backward", - "count": 61, - "label": "60d" - }, - { - "step": "day", - "stepmode": "backward", - "count": 91, - "label": "90d" - }, - { - "step": "day", - "stepmode": "backward", - "count": 181, - "label": "all" - } - ] - } - }, - "yaxis": { - "hoverformat": ",.0", - "tickformat": ",.0", - "gridcolor": "#FFF", - "gridwidth": 2, - "autotick": true, - "rangemode": "tozero", - "showline": true, - "title": "Downloads", - "ticksuffix": "", - "tickmode": "auto", - "linecolor": "rgba(148, 148, 148, 1)", - "linewidth": 2, - "rangeselector": { - "buttons": [ - { - "step": "day", - "stepmode": "backward", - "count": 31, - "label": "30d" - }, - { - "step": "day", - "stepmode": "backward", - "count": 61, - "label": "60d" - }, - { - "step": "day", - "stepmode": "backward", - "count": 91, - "label": "90d" - }, - { - "step": "day", - "stepmode": "backward", - "count": 181, - "label": "all" - } - ] - } - } - }, - "config": { - "displaylogo": false, - "modeBarButtonsToRemove": [ - "toImage", - "sendDataToCloud", - "zoom2d", - "pan2d", - "select2d", - "lasso2d", - "zoomIn2d", - "zoomOut2d", - "toggleSpikelines" - ] - } - }, - "percentages": { - "layout": { - "autosize": true, - "height": 400, - "margin": { - "r": 100, - "t": 80, - "autoexpand": true, - "b": 80, - "l": 100, - "pad": 0 - }, - "paper_bgcolor": "#fff", - "plot_bgcolor": "rgba(175, 175, 175, 0.2)", - "showlegend": true, - "legend": { - "orientation": "v", - "bgcolor": "#e7e7e7", - "xanchor": "left", - "yanchor": "middle", - "x": 0, - "y": 0.5 - }, - "title": "Proportional Downloads", - "xaxis": { - "tickformat": "%m-%d", - "dtick": 604800000, - "tick0": "2017-08-07", - "gridcolor": "#FFF", - "gridwidth": 2, - "anchor": "y", - "domain": [ - 0, - 1 - ], - "title": "Date", - "titlefont": { - "family": "Geneva, Verdana, Geneva, sans-serif", - "size": 16, - "color": "#7f7f7f" - }, - "showline": true, - "linecolor": "rgba(148, 148, 148, 1)", - "linewidth": 2, - "tickangle": -45 - }, - "yaxis": { - "range": [ - 0, - 100 - ], - "dtick": 20, - "gridcolor": "#FFF", - "gridwidth": 2, - "autotick": false, - "showline": true, - "title": "Download Proportion", - "ticksuffix": "%", - "tickmode": "auto", - "linecolor": "rgba(148, 148, 148, 1)", - "linewidth": 2 - } - }, - "config": { - "displaylogo": false, - "modeBarButtonsToRemove": [ - "toImage", - "sendDataToCloud", - "zoom2d", - "pan2d", - "select2d", - "lasso2d", - "zoomIn2d", - "zoomOut2d", - "toggleSpikelines" - ] - } - } -} diff --git a/pypistats/run.py b/pypistats/run.py deleted file mode 100644 index 05f71b0..0000000 --- a/pypistats/run.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Run the application.""" -import os - -from flask import g -from flask import redirect -from flask import request -from flask import session -from flask_limiter import Limiter -from flask_limiter.util import get_remote_address -from werkzeug.middleware.proxy_fix import ProxyFix - -from pypistats.application import create_app -from pypistats.config import configs -from pypistats.models.user import User - -# change this for migrations -env = os.environ.get("ENV", "development") - -app = create_app(configs[env]) - -# Rate limiting per IP/worker -app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2) -limiter = Limiter(app, key_func=get_remote_address, application_limits=["5 per second", "30 per minute"]) - -app.logger.info(f"Environment: {env}") - - -@app.before_request -def before_request(): - """Execute before requests.""" - # http -> https - scheme = request.headers.get("X-Forwarded-Proto") - if scheme and scheme == "http" and request.url.startswith("http://"): - url = request.url.replace("http://", "https://", 1) - return redirect(url, code=301) - # set user - g.user = None - if "user_id" in session: - g.user = User.query.get(session["user_id"]) diff --git a/pypistats/tasks/__init__.py b/pypistats/tasks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pypistats/tasks/pypi.py b/pypistats/tasks/pypi.py deleted file mode 100644 index 62802a9..0000000 --- a/pypistats/tasks/pypi.py +++ /dev/null @@ -1,408 +0,0 @@ -"""Get the download stats for a specific day.""" -import datetime -import os -import time - -import psycopg2 -from google.auth.crypt._python_rsa import RSASigner -from google.cloud import bigquery -from google.oauth2.service_account import Credentials -from psycopg2.extras import execute_values - -from pypistats.extensions import celery - -# Mirrors to disregard when considering downloads -MIRRORS = ("bandersnatch", "z3c.pypimirror", "Artifactory", "devpi") - -# PyPI systems -SYSTEMS = ("Windows", "Linux", "Darwin") - -# postgresql tables to update for __all__ -PSQL_TABLES = ["overall", "python_major", "python_minor", "system"] - -# Number of days to retain records -MAX_RECORD_AGE = 180 - - -def get_google_credentials(): - """Obtain the Google credentials object explicitly.""" - private_key = os.environ["GOOGLE_PRIVATE_KEY"].replace('"', "").replace("\\n", "\n") - private_key_id = os.environ["GOOGLE_PRIVATE_KEY_ID"] - signer = RSASigner.from_string(key=private_key, key_id=private_key_id) - - project_id = os.environ["GOOGLE_PROJECT_ID"] - service_account_email = os.environ["GOOGLE_CLIENT_EMAIL"] - scopes = ("https://www.googleapis.com/auth/bigquery", "https://www.googleapis.com/auth/cloud-platform") - token_uri = os.environ["GOOGLE_TOKEN_URI"] - credentials = Credentials( - signer=signer, - service_account_email=service_account_email, - token_uri=token_uri, - scopes=scopes, - project_id=project_id, - ) - return credentials - - -def get_daily_download_stats(date): - """Get daily download stats for pypi packages from BigQuery.""" - start = time.time() - - job_config = bigquery.QueryJobConfig() - credentials = get_google_credentials() - bq_client = bigquery.Client(project=os.environ["GOOGLE_PROJECT_ID"], credentials=credentials) - if date is None: - date = str(datetime.date.today() - datetime.timedelta(days=1)) - - print(date) - print("Sending query to BigQuery...") - query = get_query(date) - print(query) - print("Sent.") - query_job = bq_client.query(query, job_config=job_config) - iterator = query_job.result() - print("Downloading results.") - rows = list(iterator) - print(len(rows), "rows from gbq") - - data = {} - for row in rows: - if row["category_label"] not in data: - data[row["category_label"]] = [] - data[row["category_label"]].append([date, row["package"], row["category"], row["downloads"]]) - - results = update_db(data, date) - print("Elapsed: " + str(time.time() - start)) - results["elapsed"] = time.time() - start - return results - - -def update_db(data, date=None): - """Update the db with new data by table.""" - connection, cursor = get_connection_cursor() - - success = {} - for category_label, rows in data.items(): - table = category_label - success[table] = update_table(connection, cursor, table, rows, date) - - return success - - -def update_table(connection, cursor, table, rows, date): - """Update a table.""" - print(table) - - delete_rows = [] - for row_idx, row in enumerate(rows): - for idx, item in enumerate(row): - if item is None: - row[idx] = "null" - else: - # Some hacky packages have long names; ignore them - if len(str(item)) > 128: - delete_rows.append(row_idx) - print(row) - - # Some packages have installs with empty (non-null) python version; ignore - if table in ("python_major", "python_minor"): - for idx, row in enumerate(rows): - if row[2] in ("", "."): - delete_rows.append(idx) - print(row) - - print(delete_rows) - # Delete ignored rows - for idx in sorted(delete_rows, reverse=True): - rows.pop(idx) - - delete_query = f"""DELETE FROM {table} - WHERE date = '{date}'""" - insert_query = f"""INSERT INTO {table} (date, package, category, downloads) - VALUES %s""" - - try: - print(delete_query) - cursor.execute(delete_query) - print(insert_query) - execute_values(cursor, insert_query, rows) - connection.commit() - return True - except psycopg2.IntegrityError as e: - connection.rollback() - return False - - -def update_all_package_stats(date=None): - """Update stats for __all__ packages.""" - print("__all__") - start = time.time() - - if date is None: - date = str(datetime.date.today() - datetime.timedelta(days=1)) - - connection, cursor = get_connection_cursor() - - success = {} - for table in PSQL_TABLES: - aggregate_query = f"""SELECT date, '__all__' AS package, category, sum(downloads) AS downloads - FROM {table} where date = '{date}' GROUP BY date, category""" - cursor.execute(aggregate_query, (table,)) - values = cursor.fetchall() - - delete_query = f"""DELETE FROM {table} - WHERE date = '{date}' and package = '__all__'""" - insert_query = f"""INSERT INTO {table} (date, package, category, downloads) - VALUES %s""" - try: - print(delete_query) - cursor.execute(delete_query) - print(insert_query) - execute_values(cursor, insert_query, values) - connection.commit() - success[table] = True - except psycopg2.IntegrityError as e: - connection.rollback() - success[table] = False - - print("Elapsed: " + str(time.time() - start)) - success["elapsed"] = time.time() - start - return success - - -def update_recent_stats(date=None): - """Update daily, weekly, monthly stats for all packages.""" - print("recent") - start = time.time() - - if date is None: - date = str(datetime.date.today() - datetime.timedelta(days=1)) - - connection, cursor = get_connection_cursor() - - downloads_table = "overall" - recent_table = "recent" - - date = datetime.datetime.strptime(date, "%Y-%m-%d").date() - date_week = date - datetime.timedelta(days=7) - date_month = date - datetime.timedelta(days=30) - - where = { - "day": f"date = '{str(date)}'", - "week": f"date > '{str(date_week)}'", - "month": f"date > '{str(date_month)}'", - } - - success = {} - for period, clause in where.items(): - select_query = f"""SELECT package, '{period}' as category, sum(downloads) AS downloads - FROM {downloads_table} - WHERE category = 'without_mirrors' and {clause} - GROUP BY package""" - cursor.execute(select_query) - values = cursor.fetchall() - - delete_query = f"""DELETE FROM {recent_table} - WHERE category = '{period}'""" - insert_query = f"""INSERT INTO {recent_table} - (package, category, downloads) VALUES %s""" - try: - print(delete_query) - cursor.execute(delete_query) - print(insert_query) - execute_values(cursor, insert_query, values) - connection.commit() - success[period] = True - except psycopg2.IntegrityError as e: - connection.rollback() - success[period] = False - - print("Elapsed: " + str(time.time() - start)) - success["elapsed"] = time.time() - start - return success - - -def get_connection_cursor(): - """Get a db connection cursor.""" - connection = psycopg2.connect( - dbname=os.environ["POSTGRESQL_DBNAME"], - user=os.environ["POSTGRESQL_USERNAME"], - password=os.environ["POSTGRESQL_PASSWORD"], - host=os.environ["POSTGRESQL_HOST"], - port=os.environ["POSTGRESQL_PORT"], - # sslmode='require', - ) - cursor = connection.cursor() - return connection, cursor - - -def purge_old_data(date=None): - """Purge old data records.""" - print("Purge") - age = MAX_RECORD_AGE - start = time.time() - - if date is None: - date = str(datetime.date.today() - datetime.timedelta(days=1)) - - connection, cursor = get_connection_cursor() - - date = datetime.datetime.strptime(date, "%Y-%m-%d") - purge_date = date - datetime.timedelta(days=age) - purge_date = purge_date.strftime("%Y-%m-%d") - - success = {} - for table in PSQL_TABLES: - delete_query = f"""DELETE FROM {table} where date < '{purge_date}'""" - try: - print(delete_query) - cursor.execute(delete_query) - connection.commit() - success[table] = True - except psycopg2.IntegrityError as e: - connection.rollback() - success[table] = False - - print("Elapsed: " + str(time.time() - start)) - success["elapsed"] = time.time() - start - return success - - -def vacuum_analyze(): - """Vacuum and analyze the db.""" - connection, cursor = get_connection_cursor() - connection.set_isolation_level(0) - - results = {} - start = time.time() - cursor.execute("VACUUM") - results["vacuum"] = time.time() - start - - start = time.time() - cursor.execute("ANALYZE") - results["analyze"] = time.time() - start - - print(results) - return results - - -def get_query(date): - """Get the query to execute against pypistats on bigquery.""" - return f""" - WITH - dls AS ( - SELECT - file.project AS package, - details.installer.name AS installer, - details.python AS python_version, - details.system.name AS system - FROM - `bigquery-public-data.pypi.file_downloads` - WHERE - DATE(timestamp) = '{date}' - AND - (REGEXP_CONTAINS(details.python,r'^[0-9]\.[0-9]+.{{0,}}$') OR - details.python IS NULL) - ) - SELECT - package, - 'python_major' AS category_label, - cast(SPLIT(python_version, '.')[ - OFFSET - (0)] as string) AS category, - COUNT(*) AS downloads - FROM - dls - WHERE - installer NOT IN {str(MIRRORS)} - GROUP BY - package, - category - UNION ALL - SELECT - package, - 'python_minor' AS category_label, - REGEXP_EXTRACT(python_version, r'^[0-9]+\.[0-9]+') AS category, - COUNT(*) AS downloads - FROM - dls - WHERE - installer NOT IN {str(MIRRORS)} - GROUP BY - package, - category - UNION ALL - SELECT - package, - 'overall' AS category_label, - 'with_mirrors' AS category, - COUNT(*) AS downloads - FROM - dls - GROUP BY - package, - category - UNION ALL - SELECT - package, - 'overall' AS category_label, - 'without_mirrors' AS category, - COUNT(*) AS downloads - FROM - dls - WHERE - installer NOT IN {str(MIRRORS)} - GROUP BY - package, - category - UNION ALL - SELECT - package, - 'system' AS category_label, - CASE - WHEN system NOT IN {str(SYSTEMS)} THEN 'other' - ELSE system - END AS category, - COUNT(*) AS downloads - FROM - dls - WHERE - installer NOT IN {str(MIRRORS)} - GROUP BY - package, - category - """ - - -@celery.task -def etl(date=None, purge=True): - """Perform the stats download.""" - if date is None: - date = str(datetime.date.today() - datetime.timedelta(days=1)) - results = dict() - results["downloads"] = get_daily_download_stats(date) - results["__all__"] = update_all_package_stats(date) - results["recent"] = update_recent_stats() - results["cleanup"] = vacuum_analyze() - if purge: - results["purge"] = purge_old_data(date) - return results - - -@celery.task -def example(thing): - print(thing) - print("Sleeping") - time.sleep(10) - print("done") - - -if __name__ == "__main__": - run_date = "2020-01-09" - print(run_date) - # print(purge_old_data(run_date)) - # vacuum_analyze() - print(get_daily_download_stats(run_date)) - print(update_all_package_stats(run_date)) - # print(update_recent_stats(run_date)) - # vacuum_analyze(env) diff --git a/pypistats/templates/about.html b/pypistats/templates/about.html deleted file mode 100644 index fb3a17b..0000000 --- a/pypistats/templates/about.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} -

About PyPI Stats

-
-

Goal

-

PyPI Stats aims to provide aggregate download information on python packages available from the Python Package - Index in lieu of having to execute queries against raw download records in Google BigQuery.

-

Data

-

Download stats are sourced from the Python Software Foundation's publicly available - download stats - on Google BigQuery. All aggregate download stats ignore known PyPI mirrors (such as - bandersnatch) unless noted - otherwise.

-

PyPI Stats retains data for 180 days.

-

API

-

A simple - JSON API - is available for aggregate download stats and time series for packages.

-

Downstream

-

-

    -
  • - pypistats is a python package that provides a client and CLI tool for - the pypistats.org JSON API -
  • -
  • - shields.io uses the pypistats.org JSON API to provide - download count badges, like this one for pypistats -
  • -
-

PyPIStats.org is also open source.

-

Who

-

PyPI Stats was created by - Christopher Flynn. -

-

- Thanks to Hugo (hugovk) for providing a client interface to the API. -

- -{% endblock %} diff --git a/pypistats/templates/admin.html b/pypistats/templates/admin.html deleted file mode 100644 index 872a627..0000000 --- a/pypistats/templates/admin.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} -

Analytics for PyPI packages

-
-
- {{ form.csrf_token }} - {{ form.date.label }} - {{ form.date(size=24) }} - -
-
- {% if not date %} -

Submit date to run backfill.

- {% endif %} - {% if date %} -
- {{ date }} submitted. - {% endif %} -{% endblock %} diff --git a/pypistats/templates/api.html b/pypistats/templates/api.html deleted file mode 100644 index 952ee3f..0000000 --- a/pypistats/templates/api.html +++ /dev/null @@ -1,269 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} -

PyPI Stats API

-
-

- PyPI Stats provides a simple JSON API for retrieving aggregate download stats and time series for packages. The - following are the valid endpoints using host: - https://pypistats.org/ -

-

NOTES

-

-

    -
  • All download stats exclude known mirrors (such as - bandersnatch) unless noted - otherwise. -
  • -
  • Time series data is retained only for 180 days.
  • -
  • All download data is updated once daily.
  • -
-

-

Etiquette

-

- If you plan on using the API to download historical data for every python package in the database (e.g. for some - personal data exploration), DON'T. This website runs on limited resources and you will degrade - the site performance by doing this. It will also take a very long time. -

-

- You are much better off extracting the data directly from the Google - BigQuery pypi downloads tables. You - can query up to 1TB of data FREE every month before having to pay. The volume of data queried for this website - falls well under that limit (each month of data is less than 100 GB queried) and you will have your data - in a relatively short amount of time. Here is a quick guide. -

-

- If you want to regularly fetch download counts for a particular package or set of packages, cache your results. - The data provided here is updated once daily, so you should not need to fetch results from the same API - endpoint more than once per day. -

-

Rate Limiting

-

- IP-based rate limiting is imposed application-wide. -

-

API Client

-

- The pypistats package is a python client and CLI tool for easily - accessing, aggregating, and formatting results from the API. To install, use pip: -

pip install -U pypistats
- Refer to the documentation for usage. -

-

Endpoints

-

/api/packages/<package>/recent

-

Retrieve the aggregate download quantities for the last day/week/month. -

-

Query arguments: -

    -
  • - period - (optional): - day - or - week - or - month. If omitted returns all values. -
  • -
- Example response: -
{
-  "data": {
-    "last_day": 1,
-    "last_month": 2,
-    "last_week": 3
-  },
-  "package": "package_name",
-  "type": "recent_downloads"
-}
-

-

/api/packages/<package>/overall

-

Retrieve the aggregate daily download time series with or without mirror downloads. -

-

Query arguments: -

    -
  • - mirrors - (optional): - true - or - false. If omitted returns both series data. -
  • - -
- Example response: -
{
-  "data": [
-    {
-      "category": "with_mirrors",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "without_mirrors",
-      "date": "2018-02-08",
-      "downloads": 1
-    }
-  ],
-  "package": "package_name",
-  "type": "overall_downloads"
-}
-

-

/api/packages/<package>/python_major

-

Retrieve the aggregate daily download time series by Python major version number. -

-

Query arguments: -

    -
  • - version - (optional): the Python major version number, e.g. - 2 - or - 3. If omitted returns all series data (including - null). -
  • - -
- Example response: -
{
-  "data": [
-    {
-      "category": "2",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "null",
-      "date": "2018-02-08",
-      "downloads": 1
-    }
-  ],
-  "package": "package_name",
-  "type": "python_major_downloads"
-}
-

-

/api/packages/<package>/python_minor

-

Retrieve the aggregate daily download time series by Python minor version number. -

-

Query arguments: -

    -
  • - version - (optional): the Python major version number, e.g. - 2.7 - or - 3.6. If omitted returns all series data (including - null). -
  • - -
- Example response: -
{
-  "data": [
-    {
-      "category": "2.6",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "2.7",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3.2",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3.3",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3.4",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3.5",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3.6",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "3.7",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "null",
-      "date": "2018-02-08",
-      "downloads": 1
-    }
-  ],
-  "package": "package_name",
-  "type": "python_minor_downloads"
-}
-

-

/api/packages/<package>/system

-

Retrieve the aggregate daily download time series by operating system. -

-

Query arguments: -

    -
  • - os - (optional): the operating system name, e.g. - windows, - linux, - darwin - or - other. If omitted returns all series data (including - null). -
  • - -
- Example response: -
{
-  "data": [
-    {
-      "category": "darwin",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "linux",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "null",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "other",
-      "date": "2018-02-08",
-      "downloads": 1
-    },
-    {
-      "category": "windows",
-      "date": "2018-02-08",
-      "downloads": 1
-    }
-  ],
-  "package": "package_name",
-  "type": "system_downloads"
-}
-

- -{% endblock %} diff --git a/pypistats/templates/faqs.html b/pypistats/templates/faqs.html deleted file mode 100644 index 0e3db81..0000000 --- a/pypistats/templates/faqs.html +++ /dev/null @@ -1,89 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} -

FAQs

-
-

- What is the source of the download data? -

-

- PyPI provides download records as a publicly available dataset on Google's BigQuery. You can access the data - with a Google Cloud account here. -

-

- When is the website data updated? -

-

- The data update begins at 01:00:00 UTC and should take about 10 minutes. -

-

- Why are there so many more downloads after July 26, 2018? -

-

- PyPI download records are generated by a service known as linehaul. The previous iteration of the service had an issue - which caused it to restart regularly due to running out of memory, resulting in a large quantity of dropped - download records. On July 26, a newer version of the service was deployed, which is much more robust and - reliable. -

-

- Why are the cumulative download counts different from the sum of the downloads from the overall chart? -

-

- The cumulative download counts consider only the download records which are not from a known set of PyPI mirror - applications, namely bandersnatch, z3c.pypimirror, Artifactory, and - devpi. In other words, the cumulative download counts take the sum of the downloads from the - Without_Mirrors dataset from the chart. -

-

- What is the difference between Without_Mirrors and With_Mirrors downloads? -

-

- The With_Mirrors and Without_Mirrors downloads are not mutually exclusive sets of download counts - like the other segmentations provided. In fact, the Without_Mirrors downloads are a subset of the - downloads in With_Mirrors. -

-

- Some entities will create a mirror, or clone, of the PyPI repository using a tool like bandersnatch - for the sake of security or availability. This means that their mirror repository regularly syncs with PyPI by - downloading all of the Python packages available (and versions thereof) that it does not already have. Those - downloads are recorded by PyPI with bandersnatch as the user-agent. You will see also that on days - in which you release a new version of your package there will be many more downloads from mirrors, as active - mirrors will sync with PyPI by downloading those new releases. -

-

- pypistats.org filters downloads from known mirrors from the version and system segmentations on the website. - Downloads by mirrors are intentionally excluded from download breakdowns because they do not - represent end-users of the software. Instead, they serve as an alternative provider to other end-users on - a separate (sometimes private) network. -

-

- The existence of mirrors means that the downloads provided by PyPI and BigQuery come with some uncertainty with - respect to the actual aggregate usage of Python packages. One might expect that mirrors will mask end-user - downloads for more commonly used packages while simultaneously inflating the download counts of less common - ones. This uncertainty is difficult to quantify because the mirrors don't report subsequent downloads back to - PyPI. -

-

- One can, however, assume that PyPI serves a significant proportion of the Python community's packaging - downloads. Hopefully significant enough that the quantities provided here are representative of their users and - relevant to package maintainers. There are other distributors, like Conda, which also serve python packages, - but their download data is currently not publicly available at the event level like PyPI's, and thus are not - incorporated into the metrics on this website. -

-

- Why disregard mirrors from aggregate data? -

-

- The intent of disregarding mirrors is to provide metrics that reflect end-user download aggregation. -

-

- What about downloads due to CI/CD tools? -

-

- Downloads from CI/CD tools are included in all metrics. There is currently no easy way to attribute downloads to - build/deployment tools. -

- -{% endblock %} diff --git a/pypistats/templates/index.html b/pypistats/templates/index.html deleted file mode 100644 index 6a8c0ef..0000000 --- a/pypistats/templates/index.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} -

Analytics for PyPI packages

-
-
- {{ form.csrf_token }} - {{ form.name.label }} - {{ form.name(size=24) }} - -
-
- {% if not search %} -

Search among - {{ "{:,.0f}".format(package_count) }} - python packages from PyPI (updated daily).

- {% else %} - Search results: - {% endif %} - {% if search %} -
- {% include "results.html" %} - {% endif %} -{% endblock %} diff --git a/pypistats/templates/layout.html b/pypistats/templates/layout.html deleted file mode 100644 index a7ebb95..0000000 --- a/pypistats/templates/layout.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - {% block title %}{% endblock %} - - - - - - - {% block plot %}{% endblock %} - {% block auth %}{% endblock %} - - - -
-
- -

PyPI Stats

-

- Search -
-
- All packages -
- Top packages -
-
- {% if user %} - {{ user.username }}'s packages - {% else %} - Track packages - {% endif %} -
-
- {% if user %} - Logout - {% endif %} -

- -
- -
- {% block body %}{% endblock %} -
- - -
- - - - - diff --git a/pypistats/templates/package.html b/pypistats/templates/package.html deleted file mode 100644 index 2021003..0000000 --- a/pypistats/templates/package.html +++ /dev/null @@ -1,105 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block plot %} - - -{% endblock %} -{% block body %} -

{{ package }}

-
- {% if user %} - {% if user.favorites and package in user.favorites %} -

- - REMOVE from my packages

- {% else %} -

- - ADD to my packages

- {% endif %} - {% endif %} - {% if package == "__all__" %} -

Download stats for __all__ indicate downloads across all packages on PyPI.

- {% else %} -

- {% if metadata %} - PyPI page -
- Home page -
- Author: - {{ metadata['info']['author'] }} - {% if metadata['info']['license'] is not none %} -
- License: - {% if metadata['info']['license'] | length > 200 %} - {{ metadata['info']['license'][:200] }}... - {% else %} - {{ metadata['info']['license'] }} - {% endif %} - {% endif %} -
- Summary: - {{ metadata['info']['summary'] }} -
- Latest version: - {{ metadata['info']['version'] }} -
- {% if metadata['requires'] %} - Required dependencies: - {% for required in metadata['requires'] %} - {{ required.lower() }} - {% if not loop.last %}|{% endif %} - {% endfor %} - {% endif %} - {% if metadata['optional'] %} -
- Optional dependencies: - {% for optional in metadata['optional'] %} - {{ optional.lower() }} - {% if not loop.last %}|{% endif %} - {% endfor %} - {% endif %} - {% else %} - No metadata found. - {% endif %} - {% endif %} -
-
-Downloads last day: -{{ "{:,.0f}".format(recent['day']) }} -
-Downloads last week: -{{ "{:,.0f}".format(recent['week']) }} -
-Downloads last month: -{{ "{:,.0f}".format(recent['month']) }} -

- -{% endblock %} diff --git a/pypistats/templates/results.html b/pypistats/templates/results.html deleted file mode 100644 index 226723d..0000000 --- a/pypistats/templates/results.html +++ /dev/null @@ -1,12 +0,0 @@ -
-{% if packages %} -
    - {% for package in packages %} -
  • - {{ package }} -
  • - {% endfor %} -
-{% else %} - No results. -{% endif %} \ No newline at end of file diff --git a/pypistats/templates/search.html b/pypistats/templates/search.html deleted file mode 100644 index 6e2c39a..0000000 --- a/pypistats/templates/search.html +++ /dev/null @@ -1 +0,0 @@ -{% include "index.html" %} \ No newline at end of file diff --git a/pypistats/templates/top.html b/pypistats/templates/top.html deleted file mode 100644 index c4e2f11..0000000 --- a/pypistats/templates/top.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} -

Most downloaded PyPI packages

-
- - - {% for best in top %} - - {% endfor %} - - - {% for best in top %} - - {% endfor %} - -
- Most downloaded past - {{ best['category'].lower() }}. -
- - {% for package in best['packages'] %} - - - - - - {% endfor %} -
- {{ loop.index }} - - {{ package['package'] }} - - {{ "{:,.0f}".format(package['downloads']) }} -
-
-{% endblock %} \ No newline at end of file diff --git a/pypistats/templates/user.html b/pypistats/templates/user.html deleted file mode 100644 index 6741806..0000000 --- a/pypistats/templates/user.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "layout.html" %} -{% block title %}PyPI Download Stats{% endblock %} -{% block body %} - {% if user %} - -

- {{ user.username }}'s Packages

-
-

Currently saved packages.

- {% if user.favorites %} -

-

    - {% for package in user.favorites %} -
  • - {{ package }} -
  • - {% endfor %} -
-

- {% else %} -

Not tracking any packages.

- {% endif %} - {% else %} -

My Packages

-
-

Log in with GitHub OAuth to track your own set of packages.

-

- Log in -

- {% endif %} -{% endblock %} \ No newline at end of file diff --git a/pypistats/views/__init__.py b/pypistats/views/__init__.py deleted file mode 100644 index b334de3..0000000 --- a/pypistats/views/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""The view blueprint modules.""" -from pypistats.views import admin -from pypistats.views import api -from pypistats.views import error -from pypistats.views import general -from pypistats.views import user diff --git a/pypistats/views/admin.py b/pypistats/views/admin.py deleted file mode 100644 index 6ba593a..0000000 --- a/pypistats/views/admin.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -from flask import Blueprint -from flask import render_template -from flask_wtf import FlaskForm -from werkzeug.security import check_password_hash -from werkzeug.security import generate_password_hash -from wtforms import DateField -from wtforms.validators import DataRequired - -from pypistats.extensions import auth -from pypistats.tasks.pypi import etl - -users = {os.environ["BASIC_AUTH_USER"]: generate_password_hash(os.environ["BASIC_AUTH_PASSWORD"])} - - -blueprint = Blueprint("admin", __name__, template_folder="templates") - - -@auth.verify_password -def verify_password(username, password): - if username in users and check_password_hash(users.get(username), password): - return username - - -class BackfillDateForm(FlaskForm): - date = DateField("Date: ", validators=[DataRequired()]) - - -@blueprint.route("/admin", methods=("GET", "POST")) -@auth.login_required -def index(): - form = BackfillDateForm() - if form.validate_on_submit(): - date = form.date.data - etl.apply_async(args=(str(date),)) - return render_template("admin.html", form=form, date=date) - return render_template("admin.html", form=form) diff --git a/pypistats/views/api.py b/pypistats/views/api.py deleted file mode 100644 index 6ec8a2b..0000000 --- a/pypistats/views/api.py +++ /dev/null @@ -1,148 +0,0 @@ -"""JSON API routes.""" -from flask import Blueprint -from flask import abort -from flask import g -from flask import jsonify -from flask import render_template -from flask import request - -from pypistats.models.download import RECENT_CATEGORIES -from pypistats.models.download import OverallDownloadCount -from pypistats.models.download import PythonMajorDownloadCount -from pypistats.models.download import PythonMinorDownloadCount -from pypistats.models.download import RecentDownloadCount -from pypistats.models.download import SystemDownloadCount - -blueprint = Blueprint("api", __name__, url_prefix="/api") - - -@blueprint.route("/") -def api(): - """Get API documentation.""" - return render_template("api.html", user=g.user) - - -@blueprint.route("/packages//recent") -def api_downloads_recent(package): - """Get the recent downloads of a package.""" - # abort(503) - if package != "__all__": - package = package.replace(".", "-").replace("_", "-") - category = request.args.get("period") - if category is None: - downloads = RecentDownloadCount.query.filter_by(package=package).all() - elif category in RECENT_CATEGORIES: - downloads = RecentDownloadCount.query.filter_by(package=package, category=category).all() - else: - abort(404) - - response = {"package": package, "type": "recent_downloads"} - if len(downloads) > 0: - if category is None: - response["data"] = {"last_" + rc: 0 for rc in RECENT_CATEGORIES} - else: - response["data"] = {"last_" + category: 0} - for r in downloads: - response["data"]["last_" + r.category] = r.downloads - else: - abort(404) - - return jsonify(response) - - -@blueprint.route("/packages//overall") -def api_downloads_overall(package): - """Get the overall download time series of a package.""" - # abort(503) - if package != "__all__": - package = package.replace(".", "-").replace("_", "-") - mirrors = request.args.get("mirrors") - if mirrors == "true": - downloads = ( - OverallDownloadCount.query.filter_by(package=package, category="with_mirrors") - .order_by(OverallDownloadCount.date) - .all() - ) - elif mirrors == "false": - downloads = ( - OverallDownloadCount.query.filter_by(package=package, category="without_mirrors") - .order_by(OverallDownloadCount.date) - .all() - ) - else: - downloads = ( - OverallDownloadCount.query.filter_by(package=package) - .order_by(OverallDownloadCount.category, OverallDownloadCount.date) - .all() - ) - - response = {"package": package, "type": "overall_downloads"} - if len(downloads) > 0: - response["data"] = [{"date": str(r.date), "category": r.category, "downloads": r.downloads} for r in downloads] - else: - abort(404) - - return jsonify(response) - - -@blueprint.route("/packages//python_major") -def api_downloads_python_major(package): - """Get the python major download time series of a package.""" - return generic_downloads(PythonMajorDownloadCount, package, "version", "python_major") - - -@blueprint.route("/packages//python_minor") -def api_downloads_python_minor(package): - """Get the python minor download time series of a package.""" - return generic_downloads(PythonMinorDownloadCount, package, "version", "python_minor") - - -@blueprint.route("/packages//system") -def api_downloads_system(package): - """Get the system download time series of a package.""" - return generic_downloads(SystemDownloadCount, package, "os", "system") - - -def generic_downloads(model, package, arg, name): - """Generate a generic response.""" - # abort(503) - if package != "__all__": - package = package.replace(".", "-").replace("_", "-") - category = request.args.get(arg) - if category is not None: - downloads = model.query.filter_by(package=package, category=category.title()).order_by(model.date).all() - else: - downloads = model.query.filter_by(package=package).order_by(model.category, model.date).all() - - response = {"package": package, "type": f"{name}_downloads"} - if downloads is not None: - response["data"] = [{"date": str(r.date), "category": r.category, "downloads": r.downloads} for r in downloads] - else: - abort(404) - - return jsonify(response) - - -# TODO -# @blueprint.route("/top/overall") -# def api_top_packages(): -# """Get the most downloaded packages by recency.""" -# return "top overall" -# -# -# @blueprint.route("/top/python_major") -# def api_top_python_major(): -# """Get the most downloaded packages by python major version.""" -# return "top python_major" -# -# -# @blueprint.route("/top/python_minor") -# def api_top_python_minor(): -# """Get the most downloaded packages by python minor version.""" -# return "top python_minor" -# -# -# @blueprint.route("/top/system") -# def api_top_system(): -# """Get the most downloaded packages by system.""" -# return "top python_minor" diff --git a/pypistats/views/error.py b/pypistats/views/error.py deleted file mode 100644 index 9b8b2da..0000000 --- a/pypistats/views/error.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Error page handlers.""" -from flask import Blueprint -from flask import url_for - -blueprint = Blueprint("error", __name__, template_folder="templates") - - -@blueprint.app_errorhandler(400) -def handle_400(err): - """Return 400.""" - return "400", 400 - - -@blueprint.app_errorhandler(401) -def handle_401(err): - """Return 401.""" - return "401", 401 - - -@blueprint.app_errorhandler(404) -def handle_404(err): - """Return 404.""" - return "404", 404 - - -@blueprint.app_errorhandler(429) -def handle_429(err): - return f"""429 RATE LIMIT EXCEEDED""", 429 - - -@blueprint.app_errorhandler(500) -def handle_500(err): - """Return 500.""" - return "500", 500 - - -@blueprint.app_errorhandler(503) -def handle_503(err): - """Return 500.""" - return "503 TEMPORARILY DISABLED", 503 diff --git a/pypistats/views/general.py b/pypistats/views/general.py deleted file mode 100644 index def523f..0000000 --- a/pypistats/views/general.py +++ /dev/null @@ -1,320 +0,0 @@ -"""General pages.""" -import datetime -import re -from collections import defaultdict -from copy import deepcopy - -import requests -from flask import Blueprint -from flask import current_app -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask_wtf import FlaskForm -from wtforms import StringField -from wtforms.validators import DataRequired - -from pypistats.models.download import RECENT_CATEGORIES -from pypistats.models.download import OverallDownloadCount -from pypistats.models.download import PythonMajorDownloadCount -from pypistats.models.download import PythonMinorDownloadCount -from pypistats.models.download import RecentDownloadCount -from pypistats.models.download import SystemDownloadCount - -blueprint = Blueprint("general", __name__, template_folder="templates") - - -MODELS = [OverallDownloadCount, PythonMajorDownloadCount, PythonMinorDownloadCount, SystemDownloadCount] - - -class PackageSearchForm(FlaskForm): - """Search form.""" - - name = StringField("Package: ", validators=[DataRequired()]) - - -@blueprint.route("/", methods=("GET", "POST")) -def index(): - """Render the home page.""" - form = PackageSearchForm() - if form.validate_on_submit(): - package = form.name.data - return redirect(f"/search/{package.lower()}") - package_count = RecentDownloadCount.query.filter_by(category="month").count() - return render_template("index.html", form=form, user=g.user, package_count=package_count) - - -@blueprint.route("/health") -def health(): - return "OK" - - -@blueprint.route("/search/", methods=("GET", "POST")) -def search(package): - """Render the home page.""" - package = package.replace(".", "-") - form = PackageSearchForm() - if form.validate_on_submit(): - package = form.name.data - return redirect(f"/search/{package}") - results = ( - RecentDownloadCount.query.filter( - RecentDownloadCount.package.like(f"{package}%"), RecentDownloadCount.category == "month" - ) - .order_by(RecentDownloadCount.package) - .limit(20) - .all() - ) - packages = [r.package for r in results] - if len(packages) == 1: - package = packages[0] - return redirect(f"/packages/{package}") - return render_template("search.html", search=True, form=form, packages=packages, user=g.user) - - -@blueprint.route("/about") -def about(): - """Render the about page.""" - return render_template("about.html", user=g.user) - - -@blueprint.route("/faqs") -def faqs(): - """Render the FAQs page.""" - return render_template("faqs.html", user=g.user) - - -@blueprint.route("/packages/") -def package_page(package): - """Render the package page.""" - package = package.replace(".", "-") - # Recent download stats - try: - # Take the min of the lookback and 180 - lookback = min(abs(int(request.args.get("lookback", 180))), 180) - except ValueError: - lookback = 180 - - start_date = str(datetime.date.today() - datetime.timedelta(lookback)) - - recent_downloads = RecentDownloadCount.query.filter_by(package=package).all() - - if len(recent_downloads) == 0: - return redirect(f"/search/{package}") - recent = {r: 0 for r in RECENT_CATEGORIES} - for r in recent_downloads: - recent[r.category] = r.downloads - - # PyPI metadata - metadata = None - if package != "__all__": - try: - metadata = requests.get(f"https://pypi.python.org/pypi/{package}/json", timeout=5).json() - if metadata["info"].get("requires_dist", None): - requires, optional = set(), set() - for dependency in metadata["info"]["requires_dist"]: - package_name = re.split(r"[^0-9a-zA-Z_.-]+", dependency.lower())[0] - if "; extra ==" in dependency: - optional.add(package_name) - else: - requires.add(package_name) - metadata["requires"] = sorted(requires) - metadata["optional"] = sorted(optional) - except Exception: - pass - - # Get data from db - model_data = [] - for model in MODELS: - records = ( - model.query.filter_by(package=package) - .filter(model.date >= start_date) - .order_by(model.date, model.category) - .all() - ) - - if model == OverallDownloadCount: - metrics = ["downloads"] - else: - metrics = ["downloads", "percentages"] - - for metric in metrics: - model_data.append({"metric": metric, "name": model.__tablename__, "data": data_function[metric](records)}) - - # Build the plots - plots = [] - for model in model_data: - plot = deepcopy(current_app.config["PLOT_BASE"])[model["metric"]] - - # Set data - data = [] - for category, values in model["data"].items(): - base = deepcopy(current_app.config["DATA_BASE"][model["metric"]]["data"][0]) - base["x"] = values["x"] - base["y"] = values["y"] - if model["metric"] == "percentages": - base["text"] = values["text"] - base["name"] = category.title() - data.append(base) - plot["data"] = data - - # Add titles - if model["metric"] == "percentages": - plot["layout"][ - "title" - ] = f"Daily Download Proportions of {package} package - {model['name'].title().replace('_', ' ')}" # noqa - else: - plot["layout"][ - "title" - ] = f"Daily Download Quantity of {package} package - {model['name'].title().replace('_', ' ')}" # noqa - - # Explicitly set range - plot["layout"]["xaxis"]["range"] = [str(records[0].date - datetime.timedelta(1)), str(datetime.date.today())] - - # Add range buttons - plot["layout"]["xaxis"]["rangeselector"] = {"buttons": []} - drange = (datetime.date.today() - records[0].date).days - for k in [30, 60, 90, 120, 9999]: - if k <= drange: - plot["layout"]["xaxis"]["rangeselector"]["buttons"].append( - {"step": "day", "stepmode": "backward", "count": k + 1, "label": f"{k}d"} - ) - else: - plot["layout"]["xaxis"]["rangeselector"]["buttons"].append( - {"step": "day", "stepmode": "backward", "count": drange + 1, "label": "all"} - ) - break - - plots.append(plot) - - return render_template("package.html", package=package, plots=plots, metadata=metadata, recent=recent, user=g.user) - - -def get_download_data(records): - """Organize the data for the absolute plots.""" - data = defaultdict(lambda: {"x": [], "y": []}) - - date_categories = [] - all_categories = [] - - prev_date = records[0].date - - for record in records: - if record.category not in all_categories: - all_categories.append(record.category) - - all_categories = sorted(all_categories) - for category in all_categories: - data[category] # set the dict value (keeps it ordered) - - for record in records: - # Fill missing intermediate dates with zeros - if record.date != prev_date: - - for category in all_categories: - if category not in date_categories: - data[category]["x"].append(str(prev_date)) - data[category]["y"].append(0) - - # Fill missing intermediate dates with zeros - days_between = (record.date - prev_date).days - date_list = [prev_date + datetime.timedelta(days=x) for x in range(1, days_between)] - - for date in date_list: - for category in all_categories: - data[category]["x"].append(str(date)) - data[category]["y"].append(0) - - # Reset - date_categories = [] - prev_date = record.date - - # Track categories for this date - date_categories.append(record.category) - - data[record.category]["x"].append(str(record.date)) - data[record.category]["y"].append(record.downloads) - else: - # Fill in missing final date with zeros - for category in all_categories: - if category not in date_categories: - data[category]["x"].append(str(records[-1].date)) - data[category]["y"].append(0) - return data - - -def get_proportion_data(records): - """Organize the data for the fill plots.""" - data = defaultdict(lambda: {"x": [], "y": [], "text": []}) - - date_categories = defaultdict(lambda: 0) - all_categories = [] - - prev_date = records[0].date - - for record in records: - if record.category not in all_categories: - all_categories.append(record.category) - - all_categories = sorted(all_categories) - for category in all_categories: - data[category] # set the dict value (keeps it ordered) - - for record in records: - if record.date != prev_date: - - total = sum(date_categories.values()) / 100 - for category in all_categories: - data[category]["x"].append(str(prev_date)) - value = date_categories[category] / total - data[category]["y"].append(value) - data[category]["text"].append("{0:.2f}%".format(value) + " = {:,}".format(date_categories[category])) - - date_categories = defaultdict(lambda: 0) - prev_date = record.date - - # Track categories for this date - date_categories[record.category] = record.downloads - else: - # Fill in missing final date with zeros - total = sum(date_categories.values()) / 100 - for category in all_categories: - if category not in date_categories: - data[category]["x"].append(str(records[-1].date)) - data[category]["y"].append(0) - data[category]["text"].append("{0:.2f}%".format(0) + " = {:,}".format(0)) - else: - data[category]["x"].append(str(records[-1].date)) - value = date_categories[category] / total - data[category]["y"].append(value) - data[category]["text"].append("{0:.2f}%".format(value) + " = {:,}".format(date_categories[category])) - - return data - - -data_function = {"downloads": get_download_data, "percentages": get_proportion_data} - - -@blueprint.route("/top") -def top(): - """Render the top packages page.""" - top_ = [] - for category in ("day", "week", "month"): - downloads = ( - RecentDownloadCount.query.filter_by(category=category) - .filter(RecentDownloadCount.package != "__all__") - .order_by(RecentDownloadCount.downloads.desc()) - .limit(20) - .all() - ) - top_.append( - {"category": category, "packages": [{"package": d.package, "downloads": d.downloads} for d in downloads]} - ) - return render_template("top.html", top=top_, user=g.user) - - -@blueprint.route("/status") -def status(): - """Return OK.""" - return "OK" diff --git a/pypistats/views/user.py b/pypistats/views/user.py deleted file mode 100644 index 2007dd5..0000000 --- a/pypistats/views/user.py +++ /dev/null @@ -1,133 +0,0 @@ -"""User page for tracking packages.""" -from flask import Blueprint -from flask import abort -from flask import flash -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import session -from flask import url_for - -from pypistats.extensions import github -from pypistats.models.download import RecentDownloadCount -from pypistats.models.user import MAX_FAVORITES -from pypistats.models.user import User - -blueprint = Blueprint("user", __name__, template_folder="templates") - - -@github.access_token_getter -def token_getter(): - """Get the token for a user.""" - this_user = g.user - if this_user is not None: - return this_user.token - - -@blueprint.route("/github-callback") -@github.authorized_handler -def authorized(oauth_token): - """Github authorization callback.""" - next_url = request.args.get("next") or url_for("user.user") - if oauth_token is None: - flash("Authorization failed.") - return redirect(next_url) - - # Ensure a user with token doesn't already exist - this_user = User.query.filter_by(token=oauth_token).first() - if this_user is None: - this_user = User(token=oauth_token) - - # Set this to use API to get user data - g.user = this_user - user_data = github.get("user") - - # extract data - uid = user_data["id"] - username = user_data["login"] - avatar_url = user_data["avatar_url"] - - # Create/update the user - this_user = User.query.filter_by(uid=uid).first() - if this_user is None: - this_user = User(token=oauth_token, uid=uid, username=username, avatar_url=avatar_url) - else: - this_user.username = username - this_user.avatar_url = avatar_url - this_user.token = oauth_token - - this_user.save() - - session["username"] = this_user.username - session["user_id"] = this_user.id - g.user = this_user - - return redirect(next_url) - - -@blueprint.route("/login") -def login(): - """Login via GitHub OAuth.""" - if session.get("user_id", None) is None: - return github.authorize() - else: - return redirect(url_for("user.user")) - - -@blueprint.route("/logout") -def logout(): - """Logout.""" - session.pop("user_id", None) - session.pop("username", None) - g.user = None - return redirect(url_for("general.index")) - - -@blueprint.route("/user") -def user(): - """Render the user's personal page.""" - return render_template("user.html", user=g.user) - - -@blueprint.route("/user/packages/") -def user_package(package): - """Handle adding and deleting packages to user's list.""" - if g.user: - # Ensure package is valid. - downloads = RecentDownloadCount.query.filter_by(package=package).all() - - # Handle add/remove to favorites - if g.user.favorites is None: - # Ensure package is valid before adding - if len(downloads) == 0: - return abort(400) - g.user.favorites = [package] - g.user.update() - return redirect(url_for("user.user")) - elif package in g.user.favorites: - favorites = g.user.favorites - favorites.remove(package) - # Workaround for sqlalchemy mutable ARRAY types - g.user.favorites = None - g.user.save() - g.user.favorites = favorites - g.user.save() - return redirect(url_for("user.user")) - else: - if len(g.user.favorites) < MAX_FAVORITES: - # Ensure package is valid before adding - if len(downloads) == 0: - return abort(400) - favorites = g.user.favorites - favorites.append(package) - favorites = sorted(favorites) - # Workaround for sqlalchemy mutable ARRAY types - g.user.favorites = None - g.user.save() - g.user.favorites = favorites - g.user.save() - return redirect(url_for("user.user")) - else: - return f"Maximum package number reached ({MAX_FAVORITES})." - return abort(400) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index b4ee384..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,63 +0,0 @@ -[tool.poetry] -name = "pypistatsorg" -version = "11" -description = "Download counts dashboard for python packages" -authors = ["Flynn "] - -[tool.poetry.dependencies] -python = "^3.7" -google-cloud-bigquery = "^1.17" -flask = "^1.1" -github-flask = "^3.2" -flask-sqlalchemy = "^2.4" -flask-migrate = "^2.5" -flask-login = "^0.4.1" -flask-wtf = "^0.14.2" -gunicorn = "^19.9" -requests = "^2.22" -celery = "^4.3" -psycopg2-binary = "^2.8" -redis = "^3.3" -flask-limiter = "^1.2.1" -flower = "^0.9.5" -flask-httpauth = "^4.1.0" - -[tool.poetry.dev-dependencies] -black = "^19.10b0" -isort = "^5.3" - -[tool.black] -line-length = 120 -target-version = ['py37'] -include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs - | \.circleci - | \.git - | \.github - | \.hg - | \.mypy_cache - | \.pytest_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ -) -''' - -[tool.isort] -force_single_line = true -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -line_length = 120 - -[build-system] -requires = ["poetry>=1.0"] -build-backend = "poetry.masonry.api" diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..cd67023 --- /dev/null +++ b/src/app.css @@ -0,0 +1,3 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..56b52ee --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,35 @@ +import { closeRedisClient, forceDisconnectRedis } from '$lib/redis.js'; +import type { Handle } from '@sveltejs/kit'; + +// Minimal server hooks without cron +if (typeof process !== 'undefined') { + // Graceful shutdown + process.on('SIGTERM', async () => { + console.log('🛑 Received SIGTERM, closing connections...'); + await closeRedisClient(); + process.exit(0); + }); + + process.on('SIGINT', async () => { + console.log('🛑 Received SIGINT, closing connections...'); + await closeRedisClient(); + process.exit(0); + }); + + // Handle uncaught exceptions + process.on('uncaughtException', async (error) => { + console.error('🛑 Uncaught Exception:', error); + await forceDisconnectRedis(); + process.exit(1); + }); + + process.on('unhandledRejection', async (reason, promise) => { + console.error('🛑 Unhandled Rejection at:', promise, 'reason:', reason); + await forceDisconnectRedis(); + process.exit(1); + }); +} + +export const handle: Handle = async ({ event, resolve }) => { + return resolve(event); +}; \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..b8f412b --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,271 @@ +import { prisma } from './prisma.js'; +import { RECENT_CATEGORIES } from './database.js'; +import { CacheManager } from './redis.js'; + +const cache = new CacheManager(); + +export type Results = { + date: string; + category: string; + downloads: number | bigint; +} + +export async function getRecentDownloads(packageName: string, category?: string): Promise { + const cacheKey = CacheManager.getRecentStatsKey(packageName); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached && !category) { + return cached; + } + + if (category && RECENT_CATEGORIES.includes(category)) { + // Compute recent from overall without mirrors + const bounds = getRecentBounds(category); + const result = await prisma.overallDownloadCount.groupBy({ + by: ['package'], + where: { + package: packageName, + category: 'without_mirrors', + date: { gte: bounds.start } + }, + _sum: { downloads: true } + }); + return result.map(r => ({ + date: new Date().toISOString().split('T')[0], + category, + downloads: r._sum.downloads || 0 + })); + } + + // Default: return day/week/month computed on the fly + const day: Results[] = await getRecentDownloads(packageName, 'day'); + const week: Results[] = await getRecentDownloads(packageName, 'week'); + const month: Results[] = await getRecentDownloads(packageName, 'month'); + const result: Results[] = [...day, ...week, ...month]; + + // Cache the result for 1 hour + await cache.set(cacheKey, result, 3600); + + return result; +} + +function getRecentBounds(category: string) { + const today = new Date(); + let start = new Date(today); + if (category === 'day') { + // include today + } else if (category === 'week') { + start = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + } else if (category === 'month') { + start = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); + } + return { start }; +} + +export async function getOverallDownloads(packageName: string, mirrors?: string) { + const cacheKey = CacheManager.getPackageKey(packageName, `overall_${mirrors || 'all'}`); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + + const whereClause: any = { + package: packageName + }; + + if (mirrors === 'true') { + whereClause.category = 'with_mirrors'; + } else if (mirrors === 'false') { + whereClause.category = 'without_mirrors'; + } + + const result = await prisma.overallDownloadCount.findMany({ + where: whereClause, + orderBy: { + date: 'asc' + } + }); + + // Cache the result for 1 hour + await cache.set(cacheKey, result, 3600); + + return result; +} + +export async function getPythonMajorDownloads(packageName: string, version?: string) { + const cacheKey = CacheManager.getPackageKey(packageName, `python_major_${version || 'all'}`); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + + const whereClause: any = { + package: packageName + }; + + if (version) { + whereClause.category = version; + } + + const result = await prisma.pythonMajorDownloadCount.findMany({ + where: whereClause, + orderBy: { + date: 'asc' + } + }); + + // Cache the result for 1 hour + await cache.set(cacheKey, result, 3600); + + return result; +} + +export async function getPythonMinorDownloads(packageName: string, version?: string) { + const cacheKey = CacheManager.getPackageKey(packageName, `python_minor_${version || 'all'}`); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + + const whereClause: any = { + package: packageName + }; + + if (version) { + whereClause.category = version; + } + + const result = await prisma.pythonMinorDownloadCount.findMany({ + where: whereClause, + orderBy: { + date: 'asc' + } + }); + + // Cache the result for 1 hour + await cache.set(cacheKey, result, 3600); + + return result; +} + +export async function getSystemDownloads(packageName: string, os?: string) { + const cacheKey = CacheManager.getPackageKey(packageName, `system_${os || 'all'}`); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + + const whereClause: any = { + package: packageName + }; + + if (os) { + whereClause.category = os; + } + + const result = await prisma.systemDownloadCount.findMany({ + where: whereClause, + orderBy: { + date: 'asc' + } + }); + + // Cache the result for 1 hour + await cache.set(cacheKey, result, 3600); + + return result; +} + +export async function searchPackages(searchTerm: string) { + const cacheKey = CacheManager.getSearchKey(searchTerm); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + + const results = await prisma.recentDownloadCount.findMany({ + where: { + package: { + startsWith: searchTerm + }, + category: 'month' + }, + select: { + package: true + }, + distinct: ['package'], + orderBy: { + package: 'asc' + }, + take: 20 + }); + + const packages = results.map(result => result.package); + + // Cache the result for 30 minutes (search results change less frequently) + await cache.set(cacheKey, packages, 1800); + + return packages; +} + +export async function getPackageCount() { + const cacheKey = CacheManager.getPackageCountKey(); + + // Try to get from cache first + const cached = await cache.get(cacheKey); + if (cached !== null) { + return cached; + } + + const result = await prisma.recentDownloadCount.groupBy({ + by: ['package'], + where: { + category: 'month' + } + }); + + const count = result.length; + + // Cache the result for 1 hour + await cache.set(cacheKey, count, 3600); + + return count; +} + +// Cache invalidation functions +export async function invalidatePackageCache(packageName: string) { + const patterns = [ + CacheManager.getRecentStatsKey(packageName), + CacheManager.getPackageKey(packageName, 'overall_all'), + CacheManager.getPackageKey(packageName, 'overall_true'), + CacheManager.getPackageKey(packageName, 'overall_false'), + CacheManager.getPackageKey(packageName, 'python_major_all'), + CacheManager.getPackageKey(packageName, 'python_minor_all'), + CacheManager.getPackageKey(packageName, 'system_all'), + ]; + + for (const pattern of patterns) { + await cache.del(pattern); + } +} + +export async function invalidateSearchCache() { + // This would need to be implemented with pattern matching + // For now, we'll just clear the package count cache + await cache.del(CacheManager.getPackageCountKey()); +} + +export async function clearAllCache() { + await cache.flush(); +} \ No newline at end of file diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/lib/data-processor.ts b/src/lib/data-processor.ts new file mode 100644 index 0000000..d89d3b6 --- /dev/null +++ b/src/lib/data-processor.ts @@ -0,0 +1,742 @@ +import { prisma } from './prisma.js'; +import type { Prisma } from '@prisma/client'; +import { BigQuery } from '@google-cloud/bigquery'; +import { CacheManager, LockManager } from './redis.js'; + +// Configuration constants +const MIRRORS = ['bandersnatch', 'z3c.pypimirror', 'Artifactory', 'devpi']; +const SYSTEMS = ['Windows', 'Linux', 'Darwin']; +const MAX_RECORD_AGE = 180; + +interface DownloadRecord { + package: string; + category_label: string; + category: string; + downloads: number; +} + +interface ProcessedData { + [category: string]: Array<{ + date: string; + package: string; + category: string; + downloads: number; + }>; +} + +export class DataProcessor { + private bigquery: BigQuery; + private cache: CacheManager; + private locks: LockManager; + + constructor() { + // Initialize BigQuery client with flexible credential handling + const bigQueryConfig: any = { + projectId: process.env.GOOGLE_PROJECT_ID, + }; + + // Handle credentials from environment variable or file + if (process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON) { + // Use JSON credentials from environment variable + try { + const credentials = JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON); + bigQueryConfig.credentials = credentials; + bigQueryConfig.credentials.private_key = credentials.private_key.replace(/\\n/g, '\n'); + } catch (error) { + console.error('Failed to parse GOOGLE_APPLICATION_CREDENTIALS_JSON:', error); + throw new Error('Invalid GOOGLE_APPLICATION_CREDENTIALS_JSON format'); + } + } else if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { + // Use file path (existing behavior) + bigQueryConfig.keyFilename = process.env.GOOGLE_APPLICATION_CREDENTIALS; + } else { + // Try to use default credentials (for local development with gcloud auth) + console.log('No explicit credentials provided, using default credentials'); + } + + this.bigquery = new BigQuery(bigQueryConfig); + + // Initialize cache and locks + this.cache = new CacheManager(); + this.locks = new LockManager(); + } + + /** + * Main ETL process - replicates the Python etl() function + */ + async etl(date?: string, purge: boolean = true) { + const targetDate = date || this.getYesterdayDate(); + + console.log(`Starting ETL process for date: ${targetDate}`); + const etlLockKey = `pypistats:lock:etl:${targetDate}`; + const processedKey = `pypistats:processed:${targetDate}`; + let lockToken: string | null = null; + + const results: any = {}; + + try { + // If we've already processed this date, skip idempotently + const alreadyProcessed = await this.cache.get(processedKey); + if (alreadyProcessed) { + console.log(`Date ${targetDate} already processed, skipping ETL.`); + return { skipped: true }; + } + + // Acquire a short-lived lock to avoid concurrent ETL for the same date + lockToken = await this.locks.acquireLock(etlLockKey, 60 * 30); // 30 minutes + if (!lockToken) { + console.log(`Another ETL is running for ${targetDate}, skipping.`); + return { locked: true }; + } + + // Get daily download stats + results.downloads = await this.getDailyDownloadStats(targetDate); + + // Update __all__ package stats + results.__all__ = await this.updateAllPackageStats(targetDate); + + // Update recent stats + results.recent = await this.updateRecentStats(); + + // Database maintenance + results.cleanup = await this.vacuumAnalyze(); + + // Purge old data + if (purge) { + results.purge = await this.purgeOldData(targetDate); + } + + // Mark processed and clear cache + await this.cache.set(processedKey, true, 60 * 60 * 24 * 14); // remember for 14 days + await this.clearCache(); + + console.log('ETL process completed successfully'); + return results; + } catch (error) { + console.error('ETL process failed:', error); + throw error; + } finally { + // Best-effort release; if no lock held, it is a no-op + try { + if (lockToken) { + await this.locks.releaseLock(etlLockKey, lockToken); + } + } catch {} + } + } + + /** + * Get daily download stats from BigQuery + */ + async getDailyDownloadStats(date: string): Promise { + console.log(`Fetching download stats for ${date} from BigQuery...`); + + const query = this.getBigQueryQuery(date); + const [rows] = await this.bigquery.query({ query }); + + console.log(`Retrieved ${rows.length} rows from BigQuery`); + + // Process data by category + const data: ProcessedData = {}; + for (const row of rows as DownloadRecord[]) { + if (!data[row.category_label]) { + data[row.category_label] = []; + } + data[row.category_label].push({ + date, + package: row.package, + category: row.category, + downloads: row.downloads, + }); + } + + // Update database with new data + return await this.updateDatabase(data, date); + } + + /** + * Update database with processed data + */ + async updateDatabase(data: ProcessedData, date: string): Promise { + const results: any = {}; + + for (const [category, rows] of Object.entries(data)) { + console.log(`Updating ${category} table with ${rows.length} records`); + + try { + // Wrap as a transaction to ensure idempotency and avoid partial writes + await prisma.$transaction(async (tx) => { + await this.deleteExistingRecords(category, date, tx); + await this.insertRecords(category, rows, tx); + }); + + results[category] = true; + } catch (error) { + console.error(`Error updating ${category} table:`, error); + results[category] = false; + } + } + + return results; + } + + /** + * Update stats for __all__ packages (aggregated data) + */ + async updateAllPackageStats(date: string): Promise { + console.log('Updating __all__ package stats'); + + const tables = ['overall', 'python_major', 'python_minor', 'system']; + const results: any = {}; + + for (const table of tables) { + try { + // Get aggregated data for __all__ + const aggregatedData = await this.getAggregatedData(table, date); + + // Delete existing __all__ records + await this.deleteAllPackageRecords(table, date); + + // Insert aggregated records + await this.insertAllPackageRecords(table, aggregatedData); + + results[table] = true; + } catch (error) { + console.error(`Error updating __all__ for ${table}:`, error); + results[table] = false; + } + } + + return results; + } + + /** + * Update recent stats (day, week, month) + */ + async updateRecentStats(): Promise { + console.log('Updating recent stats'); + + const periods = ['day', 'week', 'month']; + const results: any = {}; + + for (const period of periods) { + try { + const recentData = await this.getRecentData(period); + + // Delete existing records for this period + await prisma.recentDownloadCount.deleteMany({ + where: { category: period } + }); + + // Insert new records + await prisma.recentDownloadCount.createMany({ + data: recentData + }); + + results[period] = true; + } catch (error) { + console.error(`Error updating recent stats for ${period}:`, error); + results[period] = false; + } + } + + return results; + } + + /** + * Purge old data (keep only MAX_RECORD_AGE days) + */ + async purgeOldData(date: string): Promise { + console.log('Purging old data'); + + const purgeDate = new Date(); + purgeDate.setDate(purgeDate.getDate() - MAX_RECORD_AGE); + + const tables = ['overall', 'python_major', 'python_minor', 'system']; + const results: any = {}; + + for (const table of tables) { + try { + const deletedCount = await this.deleteOldRecords(table, purgeDate); + results[table] = deletedCount; + } catch (error) { + console.error(`Error purging ${table}:`, error); + results[table] = false; + } + } + + return results; + } + + /** + * Database maintenance (VACUUM and ANALYZE) + */ + async vacuumAnalyze(): Promise { + console.log('Running database maintenance'); + + const results: any = {}; + + try { + // Note: Prisma doesn't support VACUUM/ANALYZE directly + // These would need to be run via raw SQL if needed + results.vacuum = 'skipped'; // Would need raw SQL + results.analyze = 'skipped'; // Would need raw SQL + } catch (error) { + console.error('Error during database maintenance:', error); + } + + return results; + } + + /** + * Clear all cache after data update + */ + private async clearCache(): Promise { + console.log('Clearing cache after data update'); + try { + await this.cache.flush(); + console.log('Cache cleared successfully'); + } catch (error) { + console.error('Error clearing cache:', error); + } + } + + // Helper methods + private getYesterdayDate(): string { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + return yesterday.toISOString().split('T')[0]; + } + + private getBigQueryQuery(date: string): string { + return ` + WITH dls AS ( + SELECT + file.project AS package, + details.installer.name AS installer, + details.python AS python_version, + details.system.name AS system + FROM \`bigquery-public-data.pypi.file_downloads\` + WHERE DATE(timestamp) = '${date}' + AND (REGEXP_CONTAINS(details.python, r'^[0-9]\\.[0-9]+.{0,}$') OR details.python IS NULL) + ) + SELECT + package, + 'python_major' AS category_label, + COALESCE(CAST(SPLIT(python_version, '.')[OFFSET(0)] AS STRING), 'unknown') AS category, + COUNT(*) AS downloads + FROM dls + WHERE installer NOT IN (${MIRRORS.map(m => `'${m}'`).join(', ')}) + GROUP BY package, category + + UNION ALL + + SELECT + package, + 'python_minor' AS category_label, + COALESCE(REGEXP_EXTRACT(python_version, r'^[0-9]+\\.[0-9]+'), 'unknown') AS category, + COUNT(*) AS downloads + FROM dls + WHERE installer NOT IN (${MIRRORS.map(m => `'${m}'`).join(', ')}) + GROUP BY package, category + + UNION ALL + + SELECT + package, + 'overall' AS category_label, + 'with_mirrors' AS category, + COUNT(*) AS downloads + FROM dls + GROUP BY package, category + + UNION ALL + + SELECT + package, + 'overall' AS category_label, + 'without_mirrors' AS category, + COUNT(*) AS downloads + FROM dls + WHERE installer NOT IN (${MIRRORS.map(m => `'${m}'`).join(', ')}) + GROUP BY package, category + + UNION ALL + + SELECT + package, + 'system' AS category_label, + COALESCE(CASE + WHEN system NOT IN (${SYSTEMS.map(s => `'${s}'`).join(', ')}) THEN 'other' + ELSE system + END, 'other') AS category, + COUNT(*) AS downloads + FROM dls + WHERE installer NOT IN (${MIRRORS.map(m => `'${m}'`).join(', ')}) + GROUP BY package, category + `; + } + + /** + * Ensure a package has up-to-date data. If missing or stale, fetch from BigQuery. + */ + async ensurePackageFreshness(packageName: string): Promise { + const yesterday = this.getYesterdayDate(); + const last = await prisma.overallDownloadCount.findFirst({ + where: { package: packageName }, + orderBy: { date: 'desc' }, + select: { date: true } + }); + + const lastDate = last?.date ? last.date.toISOString().split('T')[0] : null; + if (lastDate === yesterday) return; // up to date + + // Determine start date (inclusive) + let startDate: string; + if (lastDate) { + const d = new Date(lastDate); + d.setDate(d.getDate() + 1); + startDate = d.toISOString().split('T')[0]; + } else { + // If no data, pull last 30 days to seed + const d = new Date(); + d.setDate(d.getDate() - 30); + startDate = d.toISOString().split('T')[0]; + } + + const endDate = yesterday; + if (new Date(startDate) > new Date(endDate)) return; + + // Lock per package to avoid duplicate ingestion + const lockKey = `pypistats:lock:pkg:${packageName}:${startDate}:${endDate}`; + const token = await this.locks.acquireLock(lockKey, 60 * 15); + if (!token) return; + try { + const data = await this.getPackageDownloadStats(packageName, startDate, endDate); + await this.updateDatabase(data, startDate); // date not used inside for deletes per date; safe + // Recompute __all__ for these dates for this package is not needed; __all__ refers to special package '__all__' + } finally { + await this.locks.releaseLock(lockKey, token); + } + } + + /** + * Query BigQuery for a package between dates (inclusive) aggregating required categories per day. + */ + private async getPackageDownloadStats(packageName: string, startDate: string, endDate: string): Promise { + const query = this.getPackageBigQueryQuery(packageName, startDate, endDate); + const [rows] = await this.bigquery.query({ query }); + const data: ProcessedData = {}; + for (const row of rows as any[]) { + const label = row.category_label as string; + if (!data[label]) data[label] = []; + data[label].push({ + date: row.date, + package: row.package, + category: row.category, + downloads: Number(row.downloads) + }); + } + return data; + } + + private getPackageBigQueryQuery(packageName: string, startDate: string, endDate: string): string { + return ` + WITH dls AS ( + SELECT + DATE(timestamp) AS date, + file.project AS package, + details.installer.name AS installer, + details.python AS python_version, + details.system.name AS system + FROM \`bigquery-public-data.pypi.file_downloads\` + WHERE DATE(timestamp) BETWEEN '${startDate}' AND '${endDate}' + AND file.project = '${packageName}' + AND (REGEXP_CONTAINS(details.python, r'^[0-9]\\.[0-9]+.{0,}$') OR details.python IS NULL) + ) + SELECT + date, + package, + 'python_major' AS category_label, + COALESCE(CAST(SPLIT(python_version, '.')[OFFSET(0)] AS STRING), 'unknown') AS category, + COUNT(*) AS downloads + FROM dls + GROUP BY date, package, category + + UNION ALL + + SELECT + date, + package, + 'python_minor' AS category_label, + COALESCE(REGEXP_EXTRACT(python_version, r'^[0-9]+\\.[0-9]+'), 'unknown') AS category, + COUNT(*) AS downloads + FROM dls + GROUP BY date, package, category + + UNION ALL + + SELECT + date, + package, + 'overall' AS category_label, + 'with_mirrors' AS category, + COUNT(*) AS downloads + FROM dls + GROUP BY date, package, category + + UNION ALL + + SELECT + date, + package, + 'overall' AS category_label, + 'without_mirrors' AS category, + COUNT(*) AS downloads + FROM dls + WHERE installer NOT IN (${MIRRORS.map(m => `'${m}'`).join(', ')}) + GROUP BY date, package, category + + UNION ALL + + SELECT + date, + package, + 'system' AS category_label, + COALESCE(CASE WHEN system NOT IN (${SYSTEMS.map(s => `'${s}'`).join(', ')}) THEN 'other' ELSE system END, 'other') AS category, + COUNT(*) AS downloads + FROM dls + GROUP BY date, package, category + + UNION ALL + + SELECT + date, + package, + 'installer' AS category_label, + COALESCE(installer, 'unknown') AS category, + COUNT(*) AS downloads + FROM dls + GROUP BY date, package, category + `; + } + + private async deleteExistingRecords(table: string, date: string, tx: Prisma.TransactionClient): Promise { + const dateObj = new Date(date); + + switch (table) { + case 'overall': + await tx.overallDownloadCount.deleteMany({ + where: { date: dateObj } + }); + break; + case 'python_major': + await tx.pythonMajorDownloadCount.deleteMany({ + where: { date: dateObj } + }); + break; + case 'python_minor': + await tx.pythonMinorDownloadCount.deleteMany({ + where: { date: dateObj } + }); + break; + case 'system': + await tx.systemDownloadCount.deleteMany({ + where: { date: dateObj } + }); + break; + case 'installer': + await (tx as any).installerDownloadCount.deleteMany({ + where: { date: dateObj } + }); + break; + } + } + + private async insertRecords(table: string, records: any[], tx: Prisma.TransactionClient): Promise { + switch (table) { + case 'overall': + await tx.overallDownloadCount.createMany({ + data: records.map(r => ({ + date: new Date(r.date), + package: r.package, + category: r.category ?? 'unknown', + downloads: r.downloads + })) + }); + break; + case 'python_major': + await tx.pythonMajorDownloadCount.createMany({ + data: records.map(r => ({ + date: new Date(r.date), + package: r.package, + category: r.category ?? 'unknown', + downloads: r.downloads + })) + }); + break; + case 'python_minor': + await tx.pythonMinorDownloadCount.createMany({ + data: records.map(r => ({ + date: new Date(r.date), + package: r.package, + category: r.category ?? 'unknown', + downloads: r.downloads + })) + }); + break; + case 'system': + await tx.systemDownloadCount.createMany({ + data: records.map(r => ({ + date: new Date(r.date), + package: r.package, + category: r.category ?? 'other', + downloads: r.downloads + })) + }); + break; + case 'installer': + await (tx as any).installerDownloadCount.createMany({ + data: records.map(r => ({ + date: new Date(r.date), + package: r.package, + category: r.category ?? 'unknown', + downloads: r.downloads + })) + }); + break; + } + } + + private async getAggregatedData(table: string, date: string) { + const dateObj = new Date(date); + + switch (table) { + case 'overall': + return await prisma.overallDownloadCount.groupBy({ + by: ['date', 'category'], + where: { date: dateObj }, + _sum: { downloads: true } + }); + case 'python_major': + return await prisma.pythonMajorDownloadCount.groupBy({ + by: ['date', 'category'], + where: { date: dateObj }, + _sum: { downloads: true } + }); + case 'python_minor': + return await prisma.pythonMinorDownloadCount.groupBy({ + by: ['date', 'category'], + where: { date: dateObj }, + _sum: { downloads: true } + }); + case 'system': + return await prisma.systemDownloadCount.groupBy({ + by: ['date', 'category'], + where: { date: dateObj }, + _sum: { downloads: true } + }); + default: + return []; + } + } + + private async deleteAllPackageRecords(table: string, date: string): Promise { + const dateObj = new Date(date); + + switch (table) { + case 'overall': + await prisma.overallDownloadCount.deleteMany({ + where: { date: dateObj, package: '__all__' } + }); + break; + case 'python_major': + await prisma.pythonMajorDownloadCount.deleteMany({ + where: { date: dateObj, package: '__all__' } + }); + break; + case 'python_minor': + await prisma.pythonMinorDownloadCount.deleteMany({ + where: { date: dateObj, package: '__all__' } + }); + break; + case 'system': + await prisma.systemDownloadCount.deleteMany({ + where: { date: dateObj, package: '__all__' } + }); + break; + } + } + + private async insertAllPackageRecords(table: string, aggregatedData: any[]): Promise { + const records = aggregatedData.map(data => ({ + date: data.date, + package: '__all__', + category: data.category ?? (table === 'system' ? 'other' : 'unknown'), + downloads: data._sum.downloads || 0 + })); + + await this.insertRecords(table, records, prisma); + } + + private async getRecentData(period: string): Promise { + const today = new Date(); + let startDate: Date; + + switch (period) { + case 'day': + startDate = new Date(today); + break; + case 'week': + startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case 'month': + startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + throw new Error(`Invalid period: ${period}`); + } + + const results = await prisma.overallDownloadCount.groupBy({ + by: ['package'], + where: { + date: { gte: startDate }, + category: 'without_mirrors' + }, + _sum: { downloads: true } + }); + + return results.map(result => ({ + package: result.package, + category: period, + downloads: result._sum.downloads || 0 + })); + } + + private async deleteOldRecords(table: string, purgeDate: Date): Promise { + switch (table) { + case 'overall': + const overallResult = await prisma.overallDownloadCount.deleteMany({ + where: { date: { lt: purgeDate } } + }); + return overallResult.count; + case 'python_major': + const majorResult = await prisma.pythonMajorDownloadCount.deleteMany({ + where: { date: { lt: purgeDate } } + }); + return majorResult.count; + case 'python_minor': + const minorResult = await prisma.pythonMinorDownloadCount.deleteMany({ + where: { date: { lt: purgeDate } } + }); + return minorResult.count; + case 'system': + const systemResult = await prisma.systemDownloadCount.deleteMany({ + where: { date: { lt: purgeDate } } + }); + return systemResult.count; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/lib/database-freshness.ts b/src/lib/database-freshness.ts new file mode 100644 index 0000000..0ae8867 --- /dev/null +++ b/src/lib/database-freshness.ts @@ -0,0 +1,144 @@ +import { prisma } from './prisma.js'; + +export interface DatabaseFreshness { + isFresh: boolean; + lastUpdateDate: Date | null; + expectedDate: Date; + daysBehind: number; + needsUpdate: boolean; +} + +/** + * Check if the database is up to date with the latest data + */ +export async function checkDatabaseFreshness(): Promise { + try { + // Get the most recent date from the database + const lastUpdate = await getLastUpdateDate(); + + // Calculate the expected date (yesterday) + const expectedDate = getExpectedDate(); + + if (lastUpdate === null) { + return { + isFresh: false, + lastUpdateDate: null, + expectedDate, + daysBehind: 999, + needsUpdate: true + }; + } + + // Calculate how many days behind we are + const daysBehind = lastUpdate + ? Math.floor((expectedDate.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60 * 24)) + : 999; // If no data exists, consider it very behind + + // Determine if we need an update + // We consider it fresh if it's within 1 day of expected date + const isFresh = daysBehind <= 1; + const needsUpdate = !isFresh; + + return { + isFresh, + lastUpdateDate: lastUpdate, + expectedDate, + daysBehind, + needsUpdate + }; + } catch (error) { + console.error('Error checking database freshness:', error); + + // If we can't check, assume we need an update + return { + isFresh: false, + lastUpdateDate: null, + expectedDate: getExpectedDate(), + daysBehind: 999, + needsUpdate: true + }; + } +} + +/** + * Get the most recent date from any of our data tables + */ +async function getLastUpdateDate(): Promise { + try { + // Check multiple tables to find the most recent date + const queries = [ + prisma.overallDownloadCount.findFirst({ + orderBy: { date: 'desc' }, + select: { date: true } + }), + prisma.pythonMajorDownloadCount.findFirst({ + orderBy: { date: 'desc' }, + select: { date: true } + }), + prisma.pythonMinorDownloadCount.findFirst({ + orderBy: { date: 'desc' }, + select: { date: true } + }), + prisma.systemDownloadCount.findFirst({ + orderBy: { date: 'desc' }, + select: { date: true } + }) + ]; + + const results = await Promise.all(queries); + + // Find the most recent date across all tables + const dates = results + .map(result => result?.date) + .filter(date => date !== null) as Date[]; + + if (dates.length === 0) { + return null; + } + + return new Date(Math.max(...dates.map(date => date.getTime()))); + } catch (error) { + console.error('Error getting last update date:', error); + return null; + } +} + +/** + * Get the expected date (yesterday, since today's data might not be available yet) + */ +function getExpectedDate(): Date { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + return yesterday; +} + +/** + * Check if we have data for a specific date + */ +export async function hasDataForDate(date: Date): Promise { + try { + const count = await prisma.overallDownloadCount.count({ + where: { date } + }); + + return count > 0; + } catch (error) { + console.error('Error checking data for date:', error); + return false; + } +} + +/** + * Get a summary of database freshness for logging + */ +export async function getFreshnessSummary(): Promise { + const freshness = await checkDatabaseFreshness(); + + if (freshness.isFresh) { + return `Database is fresh (last update: ${freshness.lastUpdateDate?.toISOString().split('T')[0]})`; + } else if (freshness.lastUpdateDate) { + return `Database is ${freshness.daysBehind} days behind (last update: ${freshness.lastUpdateDate.toISOString().split('T')[0]}, expected: ${freshness.expectedDate.toISOString().split('T')[0]})`; + } else { + return 'Database has no data and needs initial population'; + } +} \ No newline at end of file diff --git a/src/lib/database.ts b/src/lib/database.ts new file mode 100644 index 0000000..03e8b04 --- /dev/null +++ b/src/lib/database.ts @@ -0,0 +1,11 @@ +// Database types and constants +export const RECENT_CATEGORIES = ['day', 'week', 'month']; + +// Re-export Prisma types for convenience +export type { + RecentDownloadCount, + OverallDownloadCount, + PythonMajorDownloadCount, + PythonMinorDownloadCount, + SystemDownloadCount +} from '@prisma/client'; \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..133a388 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = globalForPrisma.prisma ?? new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; \ No newline at end of file diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..004634e --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,341 @@ +import { createClient } from 'redis'; + +// Redis client instance +let redisClient: ReturnType | null = null; +let isConnecting = false; +let isDisconnecting = false; + +export function getRedisClient() { + if (!redisClient && !isConnecting) { + isConnecting = true; + redisClient = createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379', + }); + + redisClient.on('error', (err) => { + console.error('Redis Client Error:', err); + }); + + redisClient.on('connect', () => { + console.log('Redis Client Connected'); + isConnecting = false; + }); + + redisClient.on('disconnect', () => { + console.log('Redis Client Disconnected'); + }); + + redisClient.on('end', () => { + console.log('Redis Client Connection Ended'); + redisClient = null; + isConnecting = false; + isDisconnecting = false; + }); + + redisClient.connect().catch((error) => { + console.error('Redis Client Connection Failed:', error); + isConnecting = false; + }); + } + return redisClient; +} + +/** + * Close the Redis client connection + */ +export async function closeRedisClient(): Promise { + if (redisClient && !isDisconnecting) { + isDisconnecting = true; + try { + console.log('Closing Redis client connection...'); + await redisClient.quit(); + console.log('Redis client connection closed successfully'); + } catch (error) { + console.error('Error closing Redis client:', error); + } finally { + redisClient = null; + isDisconnecting = false; + } + } +} + +/** + * Force disconnect the Redis client (for cleanup) + */ +export async function forceDisconnectRedis(): Promise { + if (redisClient && !isDisconnecting) { + isDisconnecting = true; + try { + console.log('Force disconnecting Redis client...'); + await redisClient.disconnect(); + console.log('Redis client force disconnected'); + } catch (error) { + console.error('Error force disconnecting Redis client:', error); + } finally { + redisClient = null; + isDisconnecting = false; + } + } +} + +// Cache utilities +export class CacheManager { + private client = getRedisClient(); + private defaultTTL = 3600; // 1 hour + + async get(key: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for get operation'); + return null; + } + const value = await client.get(key); + return value ? JSON.parse(value) : null; + } catch (error) { + console.error('Redis get error:', error); + return null; + } + } + + async set(key: string, value: any, ttl: number = this.defaultTTL): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for set operation'); + return; + } + await client.setEx(key, ttl, JSON.stringify(value)); + } catch (error) { + console.error('Redis set error:', error); + } + } + + async del(key: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for del operation'); + return; + } + await client.del(key); + } catch (error) { + console.error('Redis del error:', error); + } + } + + async exists(key: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for exists operation'); + return false; + } + const result = await client.exists(key); + return result === 1; + } catch (error) { + console.error('Redis exists error:', error); + return false; + } + } + + async flush(): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for flush operation'); + return; + } + await client.flushDb(); + } catch (error) { + console.error('Redis flush error:', error); + } + } + + // Cache key generators + static getPackageKey(packageName: string, type: string): string { + return `pypistats:package:${packageName}:${type}`; + } + + static getSearchKey(query: string): string { + return `pypistats:search:${query}`; + } + + static getPackageCountKey(): string { + return 'pypistats:package_count'; + } + + static getRecentStatsKey(packageName: string): string { + return `pypistats:recent:${packageName}`; + } +} + +/** + * Distributed lock utilities backed by Redis + */ +export class LockManager { + private client = getRedisClient(); + + /** + * Try to acquire a lock for a specific key. Returns a unique token if acquired, or null if not. + */ + async acquireLock(key: string, ttlSeconds: number): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for acquireLock'); + return null; + } + const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const result = await client.set(key, token, { NX: true, EX: ttlSeconds }); + return result === 'OK' ? token : null; + } catch (error) { + console.error('Redis acquireLock error:', error); + return null; + } + } + + /** + * Release a lock only if the token matches + */ + async releaseLock(key: string, token: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for releaseLock'); + return false; + } + // Lua script to atomically check token and delete + const lua = ` + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end + `; + const res = (await client.eval(lua, { + keys: [key], + arguments: [token] + })) as number; + return res === 1; + } catch (error) { + console.error('Redis releaseLock error:', error); + return false; + } + } + + /** + * Convenience helper to run a function while holding a lock + */ + async withLock(key: string, ttlSeconds: number, fn: () => Promise): Promise { + const token = await this.acquireLock(key, ttlSeconds); + if (!token) return null; + try { + const result = await fn(); + return result; + } finally { + await this.releaseLock(key, token); + } + } +} + +// Rate limiting utilities +export class RateLimiter { + private client = getRedisClient(); + + async isRateLimited(key: string, limit: number, window: number): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for rate limiting'); + return false; + } + const current = await client.incr(key); + if (current === 1) { + await client.expire(key, window); + } + return current > limit; + } catch (error) { + console.error('Rate limiter error:', error); + return false; + } + } + + async getRemainingRequests(key: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for remaining requests check'); + return 100; + } + const current = await client.get(key); + return current ? Math.max(0, 100 - parseInt(current)) : 100; // Default limit of 100 + } catch (error) { + console.error('Get remaining requests error:', error); + return 100; + } + } +} + +// Session management utilities +export class SessionManager { + private client = getRedisClient(); + private defaultTTL = 86400; // 24 hours + + async setSession(sessionId: string, data: any): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for set session'); + return; + } + await client.setEx(sessionId, this.defaultTTL, JSON.stringify(data)); + } catch (error) { + console.error('Set session error:', error); + } + } + + async getSession(sessionId: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for get session'); + return null; + } + const data = await client.get(sessionId); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error('Get session error:', error); + return null; + } + } + + async deleteSession(sessionId: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for delete session'); + return; + } + await client.del(sessionId); + } catch (error) { + console.error('Delete session error:', error); + } + } + + async refreshSession(sessionId: string): Promise { + try { + const client = this.client; + if (!client) { + console.warn('Redis client not available for refresh session'); + return; + } + const data = await client.get(sessionId); + if (data) { + await client.setEx(sessionId, this.defaultTTL, data); + } + } catch (error) { + console.error('Refresh session error:', error); + } + } +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..d4ee706 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,51 @@ + + +
+ + + + +
+ +
+ + +
+
+
+

PyPI Stats - Download statistics for Python packages

+
+
+
+
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..7cf9899 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,15 @@ +import { getPackageCount } from '$lib/api.js'; + +export const load = async () => { + try { + const packageCount = getPackageCount(); + return { + packageCount + }; + } catch (error) { + console.error('Error loading page data:', error); + return { + packageCount: 0 + }; + } +}; \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..424fc3e --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,71 @@ + + + + PyPI Stats - Download statistics for Python packages + + + +
+
+

+ PyPI Stats +

+

+ Download statistics for Python packages +

+ + +
+
+ + +
+
+ + {#await data.packageCount then packageCount} +
+ Tracking {packageCount?.toLocaleString()} packages +
+ {/await} +
+ + +
+
+

Popular Packages

+

Check download stats for popular Python packages

+ View Examples → +
+ +
+

API Access

+

Programmatic access to download statistics

+ API Documentation → +
+ +
+

About

+

Learn more about PyPI Stats and how it works

+ Learn More → +
+
+
diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte new file mode 100644 index 0000000..c1c119f --- /dev/null +++ b/src/routes/about/+page.svelte @@ -0,0 +1,42 @@ + + About - PyPI Stats + + + +
+

About PyPI Stats

+ +
+

+ PyPI Stats provides download statistics for Python packages from the Python Package Index (PyPI). + Our data is collected from PyPI's download logs and processed to provide insights into package usage. +

+ +

What We Track

+
    +
  • Overall download counts (with and without mirrors)
  • +
  • Downloads by Python major version (2.x, 3.x)
  • +
  • Downloads by Python minor version (2.7, 3.6, 3.7, etc.)
  • +
  • Downloads by operating system (Windows, Linux, macOS)
  • +
  • Recent download statistics (day, week, month)
  • +
+ +

Data Sources

+

+ Our data comes from PyPI's BigQuery public dataset, which contains download logs from PyPI's CDN. + We process this data daily to provide up-to-date statistics. +

+ +

API Access

+

+ We provide a RESTful API for programmatic access to download statistics. + All endpoints return JSON data and are free to use. +

+ +

Privacy

+

+ We only collect aggregate statistics and do not track individual users or their download patterns. + All data is anonymized and used solely for providing download statistics. +

+
+
\ No newline at end of file diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..df81ce8 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,323 @@ + + + + Admin Dashboard - PyPI Stats + + +
+
+
+

Admin Dashboard

+

Manage data processing and cache operations

+
+ + {#if error} +
+
+
+ + + +
+
+

Error

+
{error}
+
+
+
+ {/if} + +
+ +
+

Data Processing

+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+
+ + {#if results} +
+

Results

+
+
{JSON.stringify(results, null, 2)}
+
+
+ {/if} +
+ + + + +
+

Cache Management

+ +
+
+ + +
+ +
+ + + + + +
+
+ + {#if cacheInfo} +
+

Cache Information

+
+
{JSON.stringify(cacheInfo, null, 2)}
+
+
+ {/if} +
+
+ + +
+

Environment Information

+ +
+
+

Database

+

+ {typeof process !== 'undefined' && process.env.DATABASE_URL ? 'Configured' : 'Not configured'} +

+
+ +
+

Google Cloud

+

+ {typeof process !== 'undefined' && process.env.GOOGLE_PROJECT_ID ? 'Configured' : 'Not configured'} +

+
+ +
+

Redis

+

+ {typeof process !== 'undefined' && process.env.REDIS_URL ? 'Configured' : 'Not configured'} +

+
+ +
+

Environment

+

+ {typeof process !== 'undefined' ? process.env.NODE_ENV || 'development' : 'development'} +

+
+
+
+
+
\ No newline at end of file diff --git a/pypistats/__init__.py b/src/routes/admin/+page.ts similarity index 100% rename from pypistats/__init__.py rename to src/routes/admin/+page.ts diff --git a/src/routes/api/+page.svelte b/src/routes/api/+page.svelte new file mode 100644 index 0000000..dab07a7 --- /dev/null +++ b/src/routes/api/+page.svelte @@ -0,0 +1,167 @@ + + API Documentation - PyPI Stats + + + +
+

API Documentation

+ +
+

+ The PyPI Stats API provides programmatic access to download statistics for Python packages. + All endpoints return JSON data and are free to use. +

+ +

Base URL

+
+ https://pypistats.org/api +
+ +

Endpoints

+ +
+ +
+

Recent Downloads

+
+ GET /api/packages/{package}/recent +
+

+ Get recent download statistics for a package (day, week, month). +

+
+ Parameters: +
    +
  • period (optional): Filter by period (day, week, month)
  • +
+
+
+ Example: +
+ GET /api/packages/numpy/recent?period=month +
+
+
+ + +
+

Overall Downloads

+
+ GET /api/packages/{package}/overall +
+

+ Get overall download time series for a package. +

+
+ Parameters: +
    +
  • mirrors (optional): Include mirror downloads (true/false)
  • +
+
+
+ Example: +
+ GET /api/packages/numpy/overall?mirrors=true +
+
+
+ + +
+

Python Major Version Downloads

+
+ GET /api/packages/{package}/python_major +
+

+ Get download statistics by Python major version (2.x, 3.x). +

+
+ Parameters: +
    +
  • version (optional): Filter by Python major version (2, 3)
  • +
+
+
+ Example: +
+ GET /api/packages/numpy/python_major?version=3 +
+
+
+ + +
+

Python Minor Version Downloads

+
+ GET /api/packages/{package}/python_minor +
+

+ Get download statistics by Python minor version (2.7, 3.6, 3.7, etc.). +

+
+ Parameters: +
    +
  • version (optional): Filter by Python minor version (2.7, 3.6, etc.)
  • +
+
+
+ Example: +
+ GET /api/packages/numpy/python_minor?version=3.8 +
+
+
+ + +
+

System Downloads

+
+ GET /api/packages/{package}/system +
+

+ Get download statistics by operating system (Windows, Linux, macOS). +

+
+ Parameters: +
    +
  • os (optional): Filter by operating system (Windows, Linux, Darwin)
  • +
+
+
+ Example: +
+ GET /api/packages/numpy/system?os=Linux +
+
+
+
+ +

Response Format

+

+ All API endpoints return JSON responses with the following structure: +

+
+
{`{
+  "package": "package-name",
+  "type": "endpoint_type",
+  "data": [...]
+}`}
+
+ +

Error Handling

+

+ The API uses standard HTTP status codes: +

+
    +
  • 200: Success
  • +
  • 404: Package not found
  • +
  • 500: Internal server error
  • +
+ +

Rate Limiting

+

+ We may implement rate limiting to ensure fair usage. Please be respectful of our servers + and implement appropriate caching in your applications. +

+
+
\ No newline at end of file diff --git a/src/routes/api/admin/cache/+server.ts b/src/routes/api/admin/cache/+server.ts new file mode 100644 index 0000000..a08264d --- /dev/null +++ b/src/routes/api/admin/cache/+server.ts @@ -0,0 +1,88 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { CacheManager } from '$lib/redis.js'; +import { clearAllCache, invalidatePackageCache, invalidateSearchCache } from '$lib/api.js'; + +const cache = new CacheManager(); + +export const GET: RequestHandler = async () => { + try { + // Get cache statistics + const stats = { + message: 'Cache management endpoint', + operations: ['GET', 'POST', 'DELETE'], + endpoints: { + 'GET /api/admin/cache': 'Get cache information', + 'POST /api/admin/cache/clear': 'Clear all cache', + 'POST /api/admin/cache/invalidate-package': 'Invalidate package cache', + 'POST /api/admin/cache/invalidate-search': 'Invalidate search cache' + } + }; + + return json(stats); + } catch (error) { + return json({ error: 'Failed to get cache information' }, { status: 500 }); + } +}; + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = await request.json(); + const { action, packageName } = body; + + switch (action) { + case 'clear': + await clearAllCache(); + return json({ + success: true, + message: 'All cache cleared successfully' + }); + + case 'invalidate-package': + if (!packageName) { + return json({ + error: 'Package name is required' + }, { status: 400 }); + } + await invalidatePackageCache(packageName); + return json({ + success: true, + message: `Cache invalidated for package: ${packageName}` + }); + + case 'invalidate-search': + await invalidateSearchCache(); + return json({ + success: true, + message: 'Search cache invalidated successfully' + }); + + default: + return json({ + error: 'Invalid action. Use: clear, invalidate-package, or invalidate-search' + }, { status: 400 }); + } + } catch (error) { + console.error('Cache management error:', error); + return json({ + error: 'Cache management failed', + message: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }); + } +}; + +export const DELETE: RequestHandler = async () => { + try { + await clearAllCache(); + return json({ + success: true, + message: 'All cache cleared successfully' + }); + } catch (error) { + console.error('Cache clear error:', error); + return json({ + error: 'Failed to clear cache', + message: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/api/admin/cron/+server.ts b/src/routes/api/admin/cron/+server.ts new file mode 100644 index 0000000..de33b6d --- /dev/null +++ b/src/routes/api/admin/cron/+server.ts @@ -0,0 +1,11 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +// Cron removed; keep endpoint for compatibility +export const GET: RequestHandler = async () => { + return json({ success: true, message: 'Cron removed; on-demand ingestion active.' }); +}; + +export const POST: RequestHandler = async () => { + return json({ success: true, message: 'Cron removed; on-demand ingestion active.' }); +}; \ No newline at end of file diff --git a/src/routes/api/admin/process-data/+server.ts b/src/routes/api/admin/process-data/+server.ts new file mode 100644 index 0000000..2e71f7d --- /dev/null +++ b/src/routes/api/admin/process-data/+server.ts @@ -0,0 +1,28 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { DataProcessor } from '$lib/data-processor.js'; + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = await request.json(); + const { date, purge = true } = body; + + console.log('Starting data processing via API...'); + + const processor = new DataProcessor(); + const results = await processor.etl(date, purge); + + return json({ + success: true, + message: 'Data processing completed successfully', + results + }); + } catch (error) { + console.error('Data processing failed:', error); + return json({ + success: false, + message: 'Data processing failed', + error: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/api/packages/[package]/installer/+server.ts b/src/routes/api/packages/[package]/installer/+server.ts new file mode 100644 index 0000000..1b65485 --- /dev/null +++ b/src/routes/api/packages/[package]/installer/+server.ts @@ -0,0 +1,33 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { prisma } from '$lib/prisma.js'; +import { DataProcessor } from '$lib/data-processor.js'; + +export const GET: RequestHandler = async ({ params }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + if (!packageName || packageName === '__all__') { + return json({ error: 'Invalid package name' }, { status: 400 }); + } + + try { + const processor = new DataProcessor(); + await processor.ensurePackageFreshness(packageName); + + const rows = await prisma.installerDownloadCount.findMany({ + where: { package: packageName }, + orderBy: { date: 'asc' } + }); + + const response = { + package: packageName, + type: 'installer_downloads', + data: rows.map(r => ({ date: r.date, category: r.category, downloads: r.downloads })) + }; + return json(response); + } catch (error) { + console.error('Error fetching installer downloads:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; + + diff --git a/src/routes/api/packages/[package]/overall/+server.ts b/src/routes/api/packages/[package]/overall/+server.ts new file mode 100644 index 0000000..6739a48 --- /dev/null +++ b/src/routes/api/packages/[package]/overall/+server.ts @@ -0,0 +1,35 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getOverallDownloads } from '$lib/api.js'; + +export const GET: RequestHandler = async ({ params, url }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + const mirrors = url.searchParams.get('mirrors'); + + if (packageName === '__all__') { + return json({ error: 'Invalid package name' }, { status: 400 }); + } + + try { + const downloads = await getOverallDownloads(packageName, mirrors || undefined); + + if (downloads.length === 0) { + return json({ error: 'Package not found' }, { status: 404 }); + } + + const response = { + package: packageName, + type: 'overall_downloads', + data: downloads.map(r => ({ + date: r.date, + category: r.category, + downloads: r.downloads + })) + }; + + return json(response); + } catch (error) { + console.error('Error fetching overall downloads:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/api/packages/[package]/python_major/+server.ts b/src/routes/api/packages/[package]/python_major/+server.ts new file mode 100644 index 0000000..91d0a39 --- /dev/null +++ b/src/routes/api/packages/[package]/python_major/+server.ts @@ -0,0 +1,35 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getPythonMajorDownloads } from '$lib/api.js'; + +export const GET: RequestHandler = async ({ params, url }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + const version = url.searchParams.get('version'); + + if (packageName === '__all__') { + return json({ error: 'Invalid package name' }, { status: 400 }); + } + + try { + const downloads = await getPythonMajorDownloads(packageName, version || undefined); + + if (downloads.length === 0) { + return json({ error: 'Package not found' }, { status: 404 }); + } + + const response = { + package: packageName, + type: 'python_major_downloads', + data: downloads.map(r => ({ + date: r.date, + category: r.category, + downloads: r.downloads + })) + }; + + return json(response); + } catch (error) { + console.error('Error fetching Python major downloads:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/api/packages/[package]/python_minor/+server.ts b/src/routes/api/packages/[package]/python_minor/+server.ts new file mode 100644 index 0000000..cddb8b3 --- /dev/null +++ b/src/routes/api/packages/[package]/python_minor/+server.ts @@ -0,0 +1,35 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getPythonMinorDownloads } from '$lib/api.js'; + +export const GET: RequestHandler = async ({ params, url }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + const version = url.searchParams.get('version'); + + if (packageName === '__all__') { + return json({ error: 'Invalid package name' }, { status: 400 }); + } + + try { + const downloads = await getPythonMinorDownloads(packageName, version || undefined); + + if (downloads.length === 0) { + return json({ error: 'Package not found' }, { status: 404 }); + } + + const response = { + package: packageName, + type: 'python_minor_downloads', + data: downloads.map(r => ({ + date: r.date, + category: r.category, + downloads: r.downloads + })) + }; + + return json(response); + } catch (error) { + console.error('Error fetching Python minor downloads:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/api/packages/[package]/recent/+server.ts b/src/routes/api/packages/[package]/recent/+server.ts new file mode 100644 index 0000000..1c65ab5 --- /dev/null +++ b/src/routes/api/packages/[package]/recent/+server.ts @@ -0,0 +1,70 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getRecentDownloads } from '$lib/api.js'; +import { RECENT_CATEGORIES } from '$lib/database.js'; +import { RateLimiter } from '$lib/redis.js'; +import { DataProcessor } from '$lib/data-processor.js'; + +const rateLimiter = new RateLimiter(); + +export const GET: RequestHandler = async ({ params, url, request }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + const category = url.searchParams.get('period'); + + if (packageName === '__all__') { + return json({ error: 'Invalid package name' }, { status: 400 }); + } + + // Rate limiting + const clientIP = request.headers.get('x-forwarded-for') || + request.headers.get('x-real-ip') || + 'unknown'; + const rateLimitKey = `rate_limit:recent:${clientIP}`; + + const isLimited = await rateLimiter.isRateLimited(rateLimitKey, 100, 3600); // 100 requests per hour + if (isLimited) { + return json({ + error: 'Rate limit exceeded', + message: 'Too many requests. Please try again later.' + }, { status: 429 }); + } + + try { + // Ensure package data is present/fresh on demand + const processor = new DataProcessor(); + await processor.ensurePackageFreshness(packageName); + + const downloads = await getRecentDownloads(packageName, category || undefined); + + if (downloads.length === 0) { + return json({ error: 'Package not found' }, { status: 404 }); + } + + const response: any = { + package: packageName, + type: 'recent_downloads' + }; + + if (category) { + response.data = { [`last_${category}`]: 0 }; + } else { + response.data = { [`last_${RECENT_CATEGORIES[0]}`]: 0, [`last_${RECENT_CATEGORIES[1]}`]: 0, [`last_${RECENT_CATEGORIES[2]}`]: 0 }; + } + + for (const download of downloads) { + response.data[`last_${download.category}`] = download.downloads; + } + + // Add rate limit headers + const remaining = await rateLimiter.getRemainingRequests(rateLimitKey); + const headers = { + 'X-RateLimit-Remaining': remaining.toString(), + 'X-RateLimit-Reset': (Math.floor(Date.now() / 1000) + 3600).toString() + }; + + return json(response, { headers }); + } catch (error) { + console.error('Error fetching recent downloads:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/api/packages/[package]/system/+server.ts b/src/routes/api/packages/[package]/system/+server.ts new file mode 100644 index 0000000..c404c61 --- /dev/null +++ b/src/routes/api/packages/[package]/system/+server.ts @@ -0,0 +1,35 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getSystemDownloads } from '$lib/api.js'; + +export const GET: RequestHandler = async ({ params, url }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + const os = url.searchParams.get('os'); + + if (packageName === '__all__') { + return json({ error: 'Invalid package name' }, { status: 400 }); + } + + try { + const downloads = await getSystemDownloads(packageName, os || undefined); + + if (downloads.length === 0) { + return json({ error: 'Package not found' }, { status: 404 }); + } + + const response = { + package: packageName, + type: 'system_downloads', + data: downloads.map(r => ({ + date: r.date, + category: r.category, + downloads: r.downloads + })) + }; + + return json(response); + } catch (error) { + console.error('Error fetching system downloads:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; \ No newline at end of file diff --git a/src/routes/faqs/+page.svelte b/src/routes/faqs/+page.svelte new file mode 100644 index 0000000..2c7003b --- /dev/null +++ b/src/routes/faqs/+page.svelte @@ -0,0 +1,79 @@ + + FAQs - PyPI Stats + + + +
+

Frequently Asked Questions

+ +
+
+

What is PyPI Stats?

+

+ PyPI Stats is a service that provides download statistics for Python packages from the Python Package Index (PyPI). + We collect and process download data to help developers understand package usage patterns. +

+
+ +
+

How accurate is the data?

+

+ Our data comes directly from PyPI's download logs, so it represents actual downloads from PyPI's CDN. + However, this may not capture all downloads if users are using mirrors or other distribution methods. +

+
+ +
+

How often is the data updated?

+

+ We process new data daily from PyPI's BigQuery dataset. Recent statistics (day, week, month) are updated more frequently. +

+
+ +
+

What do the different download categories mean?

+
    +
  • Overall: Total downloads, with options for including or excluding mirror downloads
  • +
  • Python Major: Downloads by Python major version (2.x vs 3.x)
  • +
  • Python Minor: Downloads by specific Python versions (2.7, 3.6, 3.7, etc.)
  • +
  • System: Downloads by operating system (Windows, Linux, macOS)
  • +
+
+ +
+

Is the API free to use?

+

+ Yes, all our API endpoints are free to use. We don't require authentication for basic usage, + though we may implement rate limiting to ensure fair usage. +

+
+ +
+

How do I use the API?

+

+ Our API provides RESTful endpoints that return JSON data. You can find detailed documentation + on our API page. +

+
+ +
+

Why don't I see data for my package?

+

+ If your package doesn't appear in our database, it might be because: +

+
    +
  • The package has very few downloads
  • +
  • The package is relatively new and hasn't been processed yet
  • +
  • There might be an issue with the package name format
  • +
+
+ +
+

Can I contribute to PyPI Stats?

+

+ Yes! PyPI Stats is open source. You can contribute by reporting bugs, suggesting features, + or submitting pull requests on our GitHub repository. +

+
+
+
\ No newline at end of file diff --git a/src/routes/packages/[package]/+page.svelte b/src/routes/packages/[package]/+page.svelte new file mode 100644 index 0000000..c2795b3 --- /dev/null +++ b/src/routes/packages/[package]/+page.svelte @@ -0,0 +1,155 @@ + + + + {data.packageName} - PyPI Stats + + + +
+
+

{data.packageName}

+

Download statistics from PyPI

+
+ + + {#if data.recentStats} +
+
+

Recent Downloads

+
+
+
+ {#each Object.entries(data.recentStats) as [period, count]} +
+
+ {(count as number).toLocaleString()} +
+
+ {period.replace('last_', '')} +
+
+ {/each} +
+
+
+ {/if} + + + {#if data.overallStats && data.overallStats.length > 0} +
+
+

Overall Downloads

+
+
+
+ + + + + + + + + + {#each data.overallStats.slice(0, 10) as stat} + + + + + + {/each} + +
DateCategoryDownloads
{stat.date}{stat.category}{stat.downloads.toLocaleString()}
+
+
+
+ {/if} + + + {#if data.pythonMajorStats && data.pythonMajorStats.length > 0} +
+
+

Python Major Version Downloads

+
+
+
+ + + + + + + + + + {#each data.pythonMajorStats.slice(0, 10) as stat} + + + + + + {/each} + +
DateVersionDownloads
{stat.date}{stat.category}{stat.downloads.toLocaleString()}
+
+
+
+ {/if} + + + {#if data.systemStats && data.systemStats.length > 0} +
+
+

System Downloads

+
+
+
+ + + + + + + + + + {#each data.systemStats.slice(0, 10) as stat} + + + + + + {/each} + +
DateSystemDownloads
{stat.date}{stat.category}{stat.downloads.toLocaleString()}
+
+
+
+ {/if} + + +
+

API Access

+
+
+ Recent downloads: + JSON +
+
+ Overall downloads: + JSON +
+
+ Python major versions: + JSON +
+
+ System downloads: + JSON +
+
+
+
\ No newline at end of file diff --git a/src/routes/packages/[package]/+page.ts b/src/routes/packages/[package]/+page.ts new file mode 100644 index 0000000..b074085 --- /dev/null +++ b/src/routes/packages/[package]/+page.ts @@ -0,0 +1,59 @@ +import { + getRecentDownloads, + getOverallDownloads, + getPythonMajorDownloads, + getPythonMinorDownloads, + getSystemDownloads +} from '$lib/api.js'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ params }) => { + const packageName = params.package?.replace(/\./g, '-').replace(/_/g, '-') || ''; + + if (!packageName || packageName === '__all__') { + return { + packageName, + recentStats: null, + overallStats: [], + pythonMajorStats: [], + pythonMinorStats: [], + systemStats: [] + }; + } + + try { + // Fetch all statistics in parallel + const [recentStats, overallStats, pythonMajorStats, pythonMinorStats, systemStats] = await Promise.all([ + getRecentDownloads(packageName), + getOverallDownloads(packageName), + getPythonMajorDownloads(packageName), + getPythonMinorDownloads(packageName), + getSystemDownloads(packageName) + ]); + + // Process recent stats into the expected format + const recentStatsFormatted: Record = {}; + for (const stat of recentStats) { + recentStatsFormatted[`last_${stat.category}`] = Number(stat.downloads); + } + + return { + packageName, + recentStats: recentStatsFormatted, + overallStats, + pythonMajorStats, + pythonMinorStats, + systemStats + }; + } catch (error) { + console.error('Error loading package data:', error); + return { + packageName, + recentStats: null, + overallStats: [], + pythonMajorStats: [], + pythonMinorStats: [], + systemStats: [] + }; + } +}; \ No newline at end of file diff --git a/src/routes/search/+page.server.ts b/src/routes/search/+page.server.ts new file mode 100644 index 0000000..e4ee949 --- /dev/null +++ b/src/routes/search/+page.server.ts @@ -0,0 +1,54 @@ +import { searchPackages } from '$lib/api.js'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ url }) => { + const searchTerm = url.searchParams.get('q'); + + if (!searchTerm) { + return { + packages: [], + searchTerm: null + }; + } + + try { + const packages = await searchPackages(searchTerm); + return { + packages, + searchTerm + }; + } catch (error) { + console.error('Error searching packages:', error); + return { + packages: [], + searchTerm + }; + } +}; + +export const actions = { + default: async ({ request }) => { + const formData = await request.formData(); + const searchTerm = formData.get('q'); + if (!searchTerm) { + return { + packages: [], + searchTerm: null + }; + } + try { + const packages = await searchPackages(searchTerm.toString()); + return { + packages, + searchTerm + }; + } catch (error) { + console.error('Error searching packages:', error); + return { + packages: [], + searchTerm + }; + } + } +}; + diff --git a/src/routes/search/+page.svelte b/src/routes/search/+page.svelte new file mode 100644 index 0000000..1ec5918 --- /dev/null +++ b/src/routes/search/+page.svelte @@ -0,0 +1,67 @@ + + + + Search Packages - PyPI Stats + + +
+
+

Search Packages

+ + +
+
+ + +
+
+ + {#if data.packages && data.packages.length > 0} +
+
+

+ Found {data.packages.length} package{data.packages.length === 1 ? '' : 's'} +

+
+
+ {#each data.packages as pkg} + + {/each} +
+
+ {:else if data.searchTerm} +
+
+

No packages found

+

Try searching for a different package name

+
+
+ {/if} +
+
\ No newline at end of file diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/pypistats/static/style.css b/static/style.css similarity index 100% rename from pypistats/static/style.css rename to static/style.css diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..301032f --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,20 @@ +import { mdsvex } from 'mdsvex'; +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: [vitePreprocess(), mdsvex()], + kit: { + adapter: adapter(), + experimental: { + // async: true, + remoteFunctions: true + } + }, + extensions: ['.svelte', '.svx'] +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2d35c4f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()] +});