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
-
-
-
- {% 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
-
-
-
- {% 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 %}
-
-
-
-
-
-
-
- {% 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 %}
-
- Most downloaded past
- {{ best['category'].lower() }} .
-
- {% endfor %}
-
-
- {% for best in top %}
-
-
- {% for package in best['packages'] %}
-
-
- {{ loop.index }}
-
-
- {{ package['package'] }}
-
-
- {{ "{:,.0f}".format(package['downloads']) }}
-
-
- {% endfor %}
-
-
- {% endfor %}
-
-
-{% 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 →
+
+
+
+
+
+
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}
+
+ {/if}
+
+
+
+
+
Data Processing
+
+
+
+ Processing Date
+
+
+
+
+
+
+ Purge old data (keep only 180 days)
+
+
+
+
+
+ {processing ? 'Processing...' : 'Process Data'}
+
+
+
+ {processing ? 'Processing...' : 'Run Cron Now'}
+
+
+
+
+ {#if results}
+
+
Results
+
+
{JSON.stringify(results, null, 2)}
+
+
+ {/if}
+
+
+
+
+
+
+
Cache Management
+
+
+
+ Package Name
+
+
+
+
+
+ Invalidate Package Cache
+
+
+
+ Invalidate Search Cache
+
+
+
+ {cacheClearing ? 'Clearing...' : 'Clear All Cache'}
+
+
+
+
+ {#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
+
+
+
+
+
+
+ Date
+ Category
+ Downloads
+
+
+
+ {#each data.overallStats.slice(0, 10) as stat}
+
+ {stat.date}
+ {stat.category}
+ {stat.downloads.toLocaleString()}
+
+ {/each}
+
+
+
+
+
+ {/if}
+
+
+ {#if data.pythonMajorStats && data.pythonMajorStats.length > 0}
+
+
+
Python Major Version Downloads
+
+
+
+
+
+
+ Date
+ Version
+ Downloads
+
+
+
+ {#each data.pythonMajorStats.slice(0, 10) as stat}
+
+ {stat.date}
+ {stat.category}
+ {stat.downloads.toLocaleString()}
+
+ {/each}
+
+
+
+
+
+ {/if}
+
+
+ {#if data.systemStats && data.systemStats.length > 0}
+
+
+
System Downloads
+
+
+
+
+
+
+ Date
+ System
+ Downloads
+
+
+
+ {#each data.systemStats.slice(0, 10) as stat}
+
+ {stat.date}
+ {stat.category}
+ {stat.downloads.toLocaleString()}
+
+ {/each}
+
+
+
+
+
+ {/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()]
+});