diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 58efb6c..f3a4933 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,8 +30,10 @@ jobs: # --output-format=github renders findings as inline PR annotations, # which is the part of the Codacy integration worth keeping. + # alembic is passed as a directory so that the versions/ exclusion in + # pyproject.toml applies; only the env.py scripts are actually checked. - name: Run ruff - run: uv run ruff check --output-format=github src tests + run: uv run ruff check --output-format=github src tests alembic - name: Run black - run: uv run black --check --diff src tests + run: uv run black --check --diff src tests alembic diff --git a/.gitignore b/.gitignore index 8e356cb..f277faa 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,17 @@ cython_debug/ # Version file generated by setuptools-scm src/targetdb/_version.py + +# Live PostgreSQL cluster created by examples/docker/docker-compose.yml. +# Never commit this: it is the server's on-disk state, and once a role has a +# password set, global/ holds its SCRAM verifier. +examples/docker/db-data/ + +# Alembic run logs kept alongside the migration directories +alembic/*/log.alembic-* + +# Local scratch and tool state +tmp/ +.pyscn/ +.pdm-python +.python-version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d81b501..6fa7035 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,15 +10,16 @@ repos: # (e.g. models/proposal.py's `partner # noqa: F401`) that F401's # autofix would silently delete. Report-only also matches the # check-only Lint workflow. - # Match the Lint workflow, which checks src and tests only. - # alembic/ migrations are historical and deliberately unlinted. - files: ^(src|tests)/ + # Match the Lint workflow, which checks src, tests and the alembic + # env.py scripts. The revision scripts under alembic/*/versions/ are + # historical and deliberately unlinted (see pyproject.toml). + files: ^(src|tests)/|^alembic/[^/]+/alembic/env\.py$ - repo: https://github.com/psf/black rev: 25.12.0 hooks: - id: black - files: ^(src|tests)/ + files: ^(src|tests)/|^alembic/[^/]+/alembic/env\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c24a648 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,56 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +See @README.md for project overview and @docs/getting_started.md for setup and usage details. + +## Commands + +```bash +# Install (development) +pip install -e . # or: uv sync + +# Run tests +pytest tests/ +pytest tests/test_models.py::test_relations_consistency # single test + +# Lint / format +ruff check src/ +black src/ + +# Docs (requires pip install -e ".[doc]" or uv sync --extra doc) +mkdocs serve +mkdocs build +``` + +## CLI + +All CLI commands require a TOML config file (`-c dbconf.toml`). The config shape is: + +```toml +[targetdb.db] +host = "localhost" +port = 5432 +dbname = "targetdb" +user = "admin" +password = "admin" # optional; omitted from the URL when absent, so libpq uses ~/.pgpass +dialect = "postgresql" +``` + +CLI flags default to `--commit False` (dry-run). Pass `--commit` explicitly to write to the database. + +## Non-obvious architecture + +- **PostgreSQL + Q3C extension required.** The Q3C extension must be installed before creating the schema (`pfs-targetdb-cli install-q3c`). It provides spatial indexing via `q3c_ang2ipix(ra, dec)` — every positional table (`target`, `fluxstd`, `sky`) has a `*_q3c_ang2ipix_idx` index. +- **Model import order in `src/targetdb/models/__init__.py` is load-order-sensitive** due to SQLAlchemy FK relationships. New models must be imported after their FK dependencies and added to `__all__`. +- **`input_catalog_id` is range-constrained.** The PostgreSQL `Identity` sequence starts at `10000`, max `89999`. Values up to `99999` are reserved for special use. +- **`version` column in `fluxstd` and `sky` is a string**, not a number (e.g., `"3.3"`). +- **All timestamps use UTC** via a custom `utcnow()` SQLAlchemy `FunctionElement` defined in `models/__init__.py`. +- **CLI delegates entirely to `utils.py`.** `cli/cli_main.py` is only argument parsing; all business logic lives in `utils.py`. +- **`TargetDB` is a subclass of `pfs.utils.database.db.DB`** (from the `pfs-utils` package, installed from GitHub). `DB` supplies the engine cache, `pool_pre_ping`, `COPY`-based bulk inserts and the `query_*` API. `targetdb.py` only overrides what `DB` lacks: a password/dialect in the URL, `update()`, `close()`, and the `dry_run=` keyword. Two overrides are load-bearing and easy to break: + - `connect()` returns `None` on purpose. `DB.connect()` hands back a pooled `Connection`, and every caller here and downstream discards it, which would leak one checked-out connection per call. + - `connection()` opens its transaction eagerly (`conn.begin()`) and rolls back when `_dry_run` is set. The eager begin matters because `pandas.to_sql` starts _and commits_ a transaction of its own when handed a connection that is not already in one, which would defeat the rollback. Every dry-run path funnels through here. `tests/integration/test_dry_run.py` guards this. +- **`TargetDB`'s connection defaults live in `DEFAULT_HOST`/`DEFAULT_USER`/`DEFAULT_DBNAME`/`DEFAULT_PORT` class attributes**, the same convention `OpDB`/`QaDB` use in `pfs.utils.database.db`. `DB.__init__` resolves any argument left `None` from `type(self).DEFAULT_*`, so every parameter in `TargetDB.__init__` must default to `None`, never to a literal — a non-`None` default in the signature shadows the class attribute and silently breaks the inherited `set_default_connection()` classmethod. `DEFAULT_USER` is `obsproc`, a read-only account, on purpose: these defaults only apply to a bare `TargetDB()`, since every write path (the CLI, `utils.add_database_rows`, …) splats a full `[targetdb.db]` table with an explicit `user`, so a least-privilege default costs nothing and makes an accidental write to production through a bare `TargetDB()` impossible. +- **psycopg3 only.** `dialect = "postgresql"` in a config file is rewritten to `postgresql+psycopg` by `normalize_drivername()` in `utils.py`, the single choke point that every code path (`get_url_object`, `TargetDB.__init__`) passes through. psycopg2 is not a dependency. +- **Inserts drop DataFrame columns the target table does not have** (`TargetDB._drop_unknown_columns`). `add_backref_values()` resolves foreign keys by merging whole reference tables in, leaving columns like `partner_name` behind; the old ORM path ignored unmapped keys and `COPY` does not. +- **Alembic has separate directories per deployment target** under `alembic/` (`local_test/`, `pfsa-db01-gb/`, `pfsa-db01-gb-dev/`). Each has its own `alembic.ini`. Run migrations from within the appropriate subdirectory. `env.py` builds the URL from the TOML file named by the `TARGETDB_CONF` environment variable, falling back to `sqlalchemy.url` in `alembic.ini` when it is unset — so credentials live in one place: `TARGETDB_CONF=dbconf.toml alembic upgrade head`. Do not use `config.set_main_option()` for the URL: ConfigParser reads `%` as an interpolation marker and would mangle passwords containing it. See `alembic/README.md` for the workflow and known gotchas (q3c expression indexes are invisible to autogenerate; `local_test/`'s revision chain is broken). diff --git a/README.md b/README.md index 9ac634e..84e429c 100644 --- a/README.md +++ b/README.md @@ -19,29 +19,28 @@ The Q3C extension is required for the database. You can install it by the follow ### Python environment Python and the following packages as well as their dependencies will be used by installing `targetdb`. -Package versions shown here are those used for the development (as of April 2025). +Package versions shown here are those used for the development (as of May 2026). Newer (and somewhat older) versions should also work. -| Package | Version | -|------------------------------------------------------------------------|--------:| -| [Python](https://www.python.org/) | 3.11.x | -| [SQLAlchemy](https://www.sqlalchemy.org/) | 2.0.x | -| [pandas](https://pandas.pydata.org/) | 2.2.3 | -| [NumPy](https://numpy.org) | 1.26.4 | -| [Astropy](https://www.astropy.org/) | 7.0.1 | -| [loguru](https://loguru.readthedocs.io/) | 0.7.3 | -| [SQLAlchemy-Utils](https://sqlalchemy-utils.readthedocs.io/en/latest/) | 0.41.2 | -| [tabulate](https://pypi.org/project/tabulate/) | 0.9.0 | -| [alembic](https://alembic.sqlalchemy.org/en/latest/) | 1.13.1 | -| [pyarrow](https://arrow.apache.org/docs/python/) | 15.0.2 | -| [Typer](https://typer.tiangolo.com/) | 0.15.2 | -| [openpyxl](https://openpyxl.readthedocs.io/en/stable/) | 3.1.2 | - -If you are using Python 3.10 or earlier, you may need to install [tomli](https://github.com/hukkin/tomli) package. - -| Package | Version | -|------------------------------------------|--------:| -| [tomli](https://github.com/hukkin/tomli) | 2.0.1 | +| Package | Version | +|------------------------------------------------------------------------|------------:| +| [Python](https://www.python.org/) | 3.12.x | +| [SQLAlchemy](https://www.sqlalchemy.org/) | 2.0.x | +| [pandas](https://pandas.pydata.org/) | 2.3.3 | +| [NumPy](https://numpy.org) | 2.4.0 | +| [Astropy](https://www.astropy.org/) | 7.2.0 | +| [loguru](https://loguru.readthedocs.io/) | 0.7.3 | +| [pfs-utils](https://github.com/Subaru-PFS/pfs_utils) | 7.2026.3100 | +| [psycopg](https://www.psycopg.org/) | 3.3.4 | +| [SQLAlchemy-Utils](https://sqlalchemy-utils.readthedocs.io/en/latest/) | 0.42.1 | +| [tabulate](https://pypi.org/project/tabulate/) | 0.9.0 | +| [alembic](https://alembic.sqlalchemy.org/en/latest/) | 1.18.0 | +| [pyarrow](https://arrow.apache.org/docs/python/) | 22.0.0 | +| [Typer](https://typer.tiangolo.com/) | 0.21.1 | +| [openpyxl](https://openpyxl.readthedocs.io/en/stable/) | 3.1.5 | + +`targetdb.TargetDB` subclasses `pfs.utils.database.db.DB`, so `pfs-utils` is installed +straight from GitHub and a `.git` directory plus network access are needed at install time. For building the documentation, the following packages are required. diff --git a/alembic/README.md b/alembic/README.md new file mode 100644 index 0000000..bdf8537 --- /dev/null +++ b/alembic/README.md @@ -0,0 +1,143 @@ +# Alembic migrations for targetdb + +Schema changes to `targetdb` are applied with +[Alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html). The models in +`src/targetdb/models/` are the source of truth; the revision scripts here record +how each deployed database was brought to match them. + +## Layout + +Each deployment target has its own directory, with its own `alembic.ini` and its +own independent revision history. **Run alembic from inside the directory for +the database you are migrating** — the histories are not interchangeable. + +| Directory | Database | +| ------------------- | ----------------------------------------------- | +| `local_test/` | A local database used for trying migrations out | +| `pfsa-db01-gb/` | Production | +| `pfsa-db01-gb-dev/` | Development | + +Directory names are historical and no longer match the current hostnames: +`pfsa-db01-gb` and `pfsa-db` resolve to different machines (`.102` and +`.110` respectively), and as of 2026-08 the production `targetdb` — the one +`TargetDB`'s own `DEFAULT_HOST`/`DEFAULT_PORT` point at — is reachable at +`pfsa-db:5433`, not `pfsa-db01-gb:5433`. Confirm which host actually holds +the database you intend to migrate before running `alembic upgrade` from +either directory. + +The revision scripts under `*/alembic/versions/` are historical records. They +are deliberately excluded from `ruff` and `black` (see `pyproject.toml`) and +should not be reformatted or "cleaned up" — only the `env.py` scripts are +linted. Once a revision has been applied to a deployed database, edit it only +to fix something that is actually broken. + +## Configuring the connection + +Set `TARGETDB_CONF` to a targetdb TOML configuration file — the same one the +CLI takes with `-c` — and `env.py` builds the connection URL from it: + +```bash +cd alembic/pfsa-db01-gb +TARGETDB_CONF=~/database_configs/config_targetdb.toml alembic upgrade head +``` + +This keeps the credentials in one place rather than duplicating them into every +`alembic.ini`, and the psycopg3 driver is selected automatically. If +`TARGETDB_CONF` is unset, `env.py` falls back to the `sqlalchemy.url` entry in +`alembic.ini`, which is a credentials-free placeholder. + +Passwords may be left out of the TOML entirely, in which case libpq resolves +them from `PGPASSWORD` or `~/.pgpass`. See `docs/getting_started.md`. + +## Workflow + +1. **Change the models** under `src/targetdb/models/`. + +2. **Generate a revision.** Autogenerate compares the models against the live + database, so this needs a database that is currently at `head`: + + ```bash + TARGETDB_CONF= alembic revision --autogenerate -m "Add columns" + ``` + +3. **Read the generated file** in `versions/` before running it. Autogenerate is + a starting point, not an answer — see the gotchas below for what it gets + wrong on this schema. + +4. **Apply it.** + + ```bash + TARGETDB_CONF= alembic upgrade head + ``` + + Check where you are at any time with `alembic current`, and what exists with + `alembic history`. + +Try a migration against `local_test/` (or a throwaway container — see +`tests/docker/`) before running it on a deployed database. + +## Gotchas + +### A unique constraint cannot be added while duplicates exist + +`CREATE UNIQUE INDEX` fails outright if the table already violates the +constraint, and the migration aborts partway: + +```text +UniqueViolation: could not create unique index "uq_sky_obj_id_input_catalog_id_version" +DETAIL: Key (obj_id, input_catalog_id, version)=(44332, 1002, 20220915) is duplicated. +``` + +Count the offending rows first, and decide what to do with them before writing +the migration: + +```sql +-- How many groups are duplicated, and by how much? +SELECT ct, count(*) AS n_groups +FROM ( + SELECT obj_id, input_catalog_id, version, count(*) AS ct + FROM sky + GROUP BY obj_id, input_catalog_id, version + HAVING count(*) > 1 +) sub +GROUP BY 1 ORDER BY 1; + +-- Which rows are they? +SELECT obj_id, ra, dec, input_catalog_id, version, count(*) +FROM sky +GROUP BY obj_id, ra, dec, input_catalog_id, version +HAVING count(*) > 1; +``` + +Substitute the table and the intended constraint columns. `sky` has run to +billions of rows per `version`, so on it and `fluxstd` expect these queries to +take a while, and restrict them by `version` where you can. + +### Autogenerate does not see the q3c indexes + +The positional tables are indexed on the expression `q3c_ang2ipix(ra, dec)`, and +SQLAlchemy cannot reflect expression-based indexes: + +```text +SAWarning: Skipped unsupported reflection of expression-based index sky_q3c_ang2ipix_idx +``` + +The warning is harmless, but it means autogenerate will neither create nor +notice a change to those indexes. Write that DDL by hand. + +Identity/serial sequences are likewise reported as skipped +(`Detected sequence named 'fluxstd_fluxstd_id_seq' ... assuming SERIAL and +omitting`); this is normal and needs nothing. + +### `%` in a password + +`env.py` builds the engine directly rather than going through +`config.set_main_option()`, because ConfigParser reads `%` as an interpolation +marker and would corrupt a password containing one. Keep it that way. + +### `local_test/` has a broken revision chain + +`80f8276e2ee7` names a `down_revision` (`ecfad41204d1`) that is not present in +the repository, so any command that walks the full revision map — `alembic +current` and `alembic history` included — raises `KeyError`. This predates the +move to `TARGETDB_CONF`; connecting to the database itself works. diff --git a/alembic/local_test/alembic.ini b/alembic/local_test/alembic.ini new file mode 100644 index 0000000..d0c7363 --- /dev/null +++ b/alembic/local_test/alembic.ini @@ -0,0 +1,77 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = 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 + +# sys.path path, will be prepended to sys.path if present. +prepend_sys_path = . + +# timezone used for the date in the migration file and its filename. +# UTC matches the rest of targetdb and keeps filenames identical whichever +# machine generates them. Resolved via dateutil, which pandas already pulls in. +timezone = UTC + +# max length of characters to apply to the "slug" field +truncate_slug_length = 40 + +# run env.py during the 'revision' command even without --autogenerate +revision_environment = false + +# allow .pyc/.pyo files without a source .py to be detected as revisions +sourceless = false + +# version path separator; "os" uses os.pathsep. +version_path_separator = os + +# the output encoding used when revision files are written from script.py.mako +output_encoding = utf-8 + +# Fallback only: env.py builds the URL from the TOML file named by the +# TARGETDB_CONF environment variable when it is set. Placeholder -- do not +# commit real credentials. +sqlalchemy.url = postgresql://username:password@localhost:5432/targetdb + + +[post_write_hooks] +# Deliberately empty: alembic/ is excluded from black and ruff in +# pyproject.toml, so generated revisions are left as alembic writes them. + + +# 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/alembic/local_test/alembic/README b/alembic/local_test/alembic/README deleted file mode 100644 index 2500aa1..0000000 --- a/alembic/local_test/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. diff --git a/alembic/local_test/alembic/env.py b/alembic/local_test/alembic/env.py index bc75cf3..433e1b7 100644 --- a/alembic/local_test/alembic/env.py +++ b/alembic/local_test/alembic/env.py @@ -1,10 +1,10 @@ from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool -from targetdb import models +from sqlalchemy import create_engine, pool from alembic import context +from targetdb import models +from targetdb.utils import get_alembic_url # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -27,6 +27,23 @@ # ... etc. +def get_url(): + """Resolve the database URL, preferring the TARGETDB_CONF TOML file. + + Point TARGETDB_CONF at a targetdb config file to keep the credentials in + one place (and out of alembic.ini): + + TARGETDB_CONF=~/database_configs/config_targetdb.toml alembic upgrade head + + Falling back to alembic.ini's sqlalchemy.url when it is unset. + + Note that config.set_main_option() is deliberately not used: ConfigParser + treats "%" as an interpolation marker, so a password containing "%" would + be mangled. Building the engine directly avoids the problem entirely. + """ + return get_alembic_url() or config.get_main_option("sqlalchemy.url") + + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -39,12 +56,13 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") + url = get_url() context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, + # compare_server_default=True, ) with context.begin_transaction(): @@ -58,14 +76,14 @@ def run_migrations_online(): and associate a connection with the context. """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) + connectable = create_engine(get_url(), poolclass=pool.NullPool) with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) + context.configure( + connection=connection, + target_metadata=target_metadata, + # compare_server_default=True, + ) with context.begin_transaction(): context.run_migrations() diff --git a/alembic/pfsa-db01-gb-dev/alembic.ini b/alembic/pfsa-db01-gb-dev/alembic.ini new file mode 100644 index 0000000..ce325d7 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic.ini @@ -0,0 +1,77 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = 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 + +# sys.path path, will be prepended to sys.path if present. +prepend_sys_path = . + +# timezone used for the date in the migration file and its filename. +# UTC matches the rest of targetdb and keeps filenames identical whichever +# machine generates them. Resolved via dateutil, which pandas already pulls in. +timezone = UTC + +# max length of characters to apply to the "slug" field +truncate_slug_length = 40 + +# run env.py during the 'revision' command even without --autogenerate +revision_environment = false + +# allow .pyc/.pyo files without a source .py to be detected as revisions +sourceless = false + +# version path separator; "os" uses os.pathsep. +version_path_separator = os + +# the output encoding used when revision files are written from script.py.mako +output_encoding = utf-8 + +# Fallback only: env.py builds the URL from the TOML file named by the +# TARGETDB_CONF environment variable when it is set. Placeholder -- do not +# commit real credentials. +sqlalchemy.url = postgresql://username:password@pfsa-db:5437/targetdb_dev + + +[post_write_hooks] +# Deliberately empty: alembic/ is excluded from black and ruff in +# pyproject.toml, so generated revisions are left as alembic writes them. + + +# 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/alembic/pfsa-db01-gb-dev/alembic/env.py b/alembic/pfsa-db01-gb-dev/alembic/env.py new file mode 100644 index 0000000..433e1b7 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/env.py @@ -0,0 +1,95 @@ +from logging.config import fileConfig + +from sqlalchemy import create_engine, pool + +from alembic import context +from targetdb import models +from targetdb.utils import get_alembic_url + +# 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. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = models.Base.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 get_url(): + """Resolve the database URL, preferring the TARGETDB_CONF TOML file. + + Point TARGETDB_CONF at a targetdb config file to keep the credentials in + one place (and out of alembic.ini): + + TARGETDB_CONF=~/database_configs/config_targetdb.toml alembic upgrade head + + Falling back to alembic.ini's sqlalchemy.url when it is unset. + + Note that config.set_main_option() is deliberately not used: ConfigParser + treats "%" as an interpolation marker, so a password containing "%" would + be mangled. Building the engine directly avoids the problem entirely. + """ + return get_alembic_url() or config.get_main_option("sqlalchemy.url") + + +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 = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + # compare_server_default=True, + ) + + 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. + + """ + connectable = create_engine(get_url(), poolclass=pool.NullPool) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + # compare_server_default=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/pfsa-db01-gb-dev/alembic/script.py.mako b/alembic/pfsa-db01-gb-dev/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${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/alembic/pfsa-db01-gb-dev/alembic/versions/20220512-103708_33e97fab1034_initial_revision.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20220512-103708_33e97fab1034_initial_revision.py new file mode 100644 index 0000000..69b8fd3 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20220512-103708_33e97fab1034_initial_revision.py @@ -0,0 +1,24 @@ +"""initial revision + +Revision ID: 33e97fab1034 +Revises: +Create Date: 2022-05-12 10:37:08.039899 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '33e97fab1034' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20220512-103828_9c71ca3a4254_add_filter_columns.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20220512-103828_9c71ca3a4254_add_filter_columns.py new file mode 100644 index 0000000..1912a7b --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20220512-103828_9c71ca3a4254_add_filter_columns.py @@ -0,0 +1,66 @@ +"""add filter columns + +Revision ID: 9c71ca3a4254 +Revises: 33e97fab1034 +Create Date: 2022-05-12 10:38:28.402465 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9c71ca3a4254' +down_revision = '33e97fab1034' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, 'cluster', ['cluster_id', 'target_id']) + op.add_column('fluxstd', sa.Column('filter_g', sa.String(), nullable=True, comment='g-band filter (g_hsc, g_ps1, g_sdss, etc.)')) + op.add_column('fluxstd', sa.Column('filter_r', sa.String(), nullable=True, comment='r-band filter (r_hsc, r_ps1, r_sdss, etc.)')) + op.add_column('fluxstd', sa.Column('filter_i', sa.String(), nullable=True, comment='i-band filter (i_hsc, i_ps1, i_sdss, etc.)')) + op.add_column('fluxstd', sa.Column('filter_z', sa.String(), nullable=True, comment='z-band filter (z_hsc, z_ps1, z_sdss, etc.)')) + op.add_column('fluxstd', sa.Column('filter_y', sa.String(), nullable=True, comment='y-band filter (y_hsc, y_ps1, y_sdss, etc.)')) + op.add_column('fluxstd', sa.Column('filter_j', sa.String(), nullable=True, comment='j-band filter (j_mko, etc.)')) + op.create_unique_constraint(None, 'fluxstd', ['fluxstd_id']) + op.create_unique_constraint(None, 'input_catalog', ['input_catalog_id']) + op.create_unique_constraint(None, 'proposal', ['proposal_id']) + op.create_unique_constraint(None, 'proposal_category', ['proposal_category_id']) + op.create_unique_constraint(None, 'sky', ['sky_id']) + op.add_column('target', sa.Column('filter_g', sa.String(), nullable=True, comment='g-band filter (g_hsc, g_ps1, g_sdss, etc.)')) + op.add_column('target', sa.Column('filter_r', sa.String(), nullable=True, comment='r-band filter (r_hsc, r_ps1, r_sdss, etc.)')) + op.add_column('target', sa.Column('filter_i', sa.String(), nullable=True, comment='i-band filter (i_hsc, i_ps1, i_sdss, etc.)')) + op.add_column('target', sa.Column('filter_z', sa.String(), nullable=True, comment='z-band filter (z_hsc, z_ps1, z_sdss, etc.)')) + op.add_column('target', sa.Column('filter_y', sa.String(), nullable=True, comment='y-band filter (y_hsc, y_ps1, y_sdss, etc.)')) + op.add_column('target', sa.Column('filter_j', sa.String(), nullable=True, comment='j-band filter (j_mko, etc.)')) + op.create_unique_constraint(None, 'target', ['target_id']) + op.create_unique_constraint(None, 'target_type', ['target_type_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'target_type', type_='unique') + op.drop_constraint(None, 'target', type_='unique') + op.drop_column('target', 'filter_j') + op.drop_column('target', 'filter_y') + op.drop_column('target', 'filter_z') + op.drop_column('target', 'filter_i') + op.drop_column('target', 'filter_r') + op.drop_column('target', 'filter_g') + op.drop_constraint(None, 'sky', type_='unique') + op.drop_constraint(None, 'proposal_category', type_='unique') + op.drop_constraint(None, 'proposal', type_='unique') + op.drop_constraint(None, 'input_catalog', type_='unique') + op.drop_constraint(None, 'fluxstd', type_='unique') + op.drop_column('fluxstd', 'filter_j') + op.drop_column('fluxstd', 'filter_y') + op.drop_column('fluxstd', 'filter_z') + op.drop_column('fluxstd', 'filter_i') + op.drop_column('fluxstd', 'filter_r') + op.drop_column('fluxstd', 'filter_g') + op.drop_constraint(None, 'cluster', type_='unique') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-172724_99f7f6b4c0d1_add_columns_for_photometric_errors_in_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-172724_99f7f6b4c0d1_add_columns_for_photometric_errors_in_.py new file mode 100644 index 0000000..5422aca --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-172724_99f7f6b4c0d1_add_columns_for_photometric_errors_in_.py @@ -0,0 +1,24 @@ +"""Add columns for photometric errors in the fluxstd table + +Revision ID: 99f7f6b4c0d1 +Revises: 9c71ca3a4254 +Create Date: 2022-10-20 17:27:24.541747 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '99f7f6b4c0d1' +down_revision = '9c71ca3a4254' +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-173436_a63ff2b68f44_add_columns_for_photometric_errors_in_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-173436_a63ff2b68f44_add_columns_for_photometric_errors_in_.py new file mode 100644 index 0000000..5f136ba --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-173436_a63ff2b68f44_add_columns_for_photometric_errors_in_.py @@ -0,0 +1,24 @@ +"""Add columns for photometric errors in the fluxstd table + +Revision ID: a63ff2b68f44 +Revises: 99f7f6b4c0d1 +Create Date: 2022-10-20 17:34:36.681006 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a63ff2b68f44' +down_revision = '99f7f6b4c0d1' +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-174527_c4e16db3e7e1_add_columns_for_photometric_errors_in_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-174527_c4e16db3e7e1_add_columns_for_photometric_errors_in_.py new file mode 100644 index 0000000..4464e97 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221020-174527_c4e16db3e7e1_add_columns_for_photometric_errors_in_.py @@ -0,0 +1,50 @@ +"""Add columns for photometric errors in the fluxstd table + +Revision ID: c4e16db3e7e1 +Revises: a63ff2b68f44 +Create Date: 2022-10-20 17:45:27.991776 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c4e16db3e7e1' +down_revision = 'a63ff2b68f44' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('fluxstd', sa.Column('psf_mag_error_g', sa.Float(), nullable=True, comment='Error in g-band PSF magnitude (AB mag)')) + op.add_column('fluxstd', sa.Column('psf_mag_error_r', sa.Float(), nullable=True, comment='Error in r-band PSF magnitude (AB mag)')) + op.add_column('fluxstd', sa.Column('psf_mag_error_i', sa.Float(), nullable=True, comment='Error in i-band PSF magnitude (AB mag)')) + op.add_column('fluxstd', sa.Column('psf_mag_error_z', sa.Float(), nullable=True, comment='Error in z-band PSF magnitude (AB mag)')) + op.add_column('fluxstd', sa.Column('psf_mag_error_y', sa.Float(), nullable=True, comment='Error in y-band PSF magnitude (AB mag)')) + op.add_column('fluxstd', sa.Column('psf_mag_error_j', sa.Float(), nullable=True, comment='Error in J band PSF magnitude (AB mag)')) + op.add_column('fluxstd', sa.Column('psf_flux_error_g', sa.Float(), nullable=True, comment='Error in g-band PSF flux (nJy)')) + op.add_column('fluxstd', sa.Column('psf_flux_error_r', sa.Float(), nullable=True, comment='Error in r-band PSF flux (nJy)')) + op.add_column('fluxstd', sa.Column('psf_flux_error_i', sa.Float(), nullable=True, comment='Error in i-band PSF flux (nJy)')) + op.add_column('fluxstd', sa.Column('psf_flux_error_z', sa.Float(), nullable=True, comment='Error in z-band PSF flux (nJy)')) + op.add_column('fluxstd', sa.Column('psf_flux_error_y', sa.Float(), nullable=True, comment='Error in y-band PSF flux (nJy)')) + op.add_column('fluxstd', sa.Column('psf_flux_error_j', sa.Float(), nullable=True, comment='Error in J band PSF flux (nJy)')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('fluxstd', 'psf_flux_error_j') + op.drop_column('fluxstd', 'psf_flux_error_y') + op.drop_column('fluxstd', 'psf_flux_error_z') + op.drop_column('fluxstd', 'psf_flux_error_i') + op.drop_column('fluxstd', 'psf_flux_error_r') + op.drop_column('fluxstd', 'psf_flux_error_g') + op.drop_column('fluxstd', 'psf_mag_error_j') + op.drop_column('fluxstd', 'psf_mag_error_y') + op.drop_column('fluxstd', 'psf_mag_error_z') + op.drop_column('fluxstd', 'psf_mag_error_i') + op.drop_column('fluxstd', 'psf_mag_error_r') + op.drop_column('fluxstd', 'psf_mag_error_g') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221111-135546_92d3534d8bf8_add_error_columns_for_psf__mag_flux_in_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221111-135546_92d3534d8bf8_add_error_columns_for_psf__mag_flux_in_.py new file mode 100644 index 0000000..abd6b83 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221111-135546_92d3534d8bf8_add_error_columns_for_psf__mag_flux_in_.py @@ -0,0 +1,50 @@ +"""Add error columns for psf_{mag,flux} in the target table + +Revision ID: 92d3534d8bf8 +Revises: c4e16db3e7e1 +Create Date: 2022-11-11 13:55:46.817819 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '92d3534d8bf8' +down_revision = 'c4e16db3e7e1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('target', sa.Column('psf_mag_error_g', sa.Float(), nullable=True, comment='Error in g-band PSF magnitude (AB mag)')) + op.add_column('target', sa.Column('psf_mag_error_r', sa.Float(), nullable=True, comment='Error in r-band PSF magnitude (AB mag)')) + op.add_column('target', sa.Column('psf_mag_error_i', sa.Float(), nullable=True, comment='Error in i-band PSF magnitude (AB mag)')) + op.add_column('target', sa.Column('psf_mag_error_z', sa.Float(), nullable=True, comment='Error in z-band PSF magnitude (AB mag)')) + op.add_column('target', sa.Column('psf_mag_error_y', sa.Float(), nullable=True, comment='Error in y-band PSF magnitude (AB mag)')) + op.add_column('target', sa.Column('psf_mag_error_j', sa.Float(), nullable=True, comment='Error in J band PSF magnitude (AB mag)')) + op.add_column('target', sa.Column('psf_flux_error_g', sa.Float(), nullable=True, comment='Error in g-band PSF flux (nJy)')) + op.add_column('target', sa.Column('psf_flux_error_r', sa.Float(), nullable=True, comment='Error in r-band PSF flux (nJy)')) + op.add_column('target', sa.Column('psf_flux_error_i', sa.Float(), nullable=True, comment='Error in i-band PSF flux (nJy)')) + op.add_column('target', sa.Column('psf_flux_error_z', sa.Float(), nullable=True, comment='Error in z-band PSF flux (nJy)')) + op.add_column('target', sa.Column('psf_flux_error_y', sa.Float(), nullable=True, comment='Error in y-band PSF flux (nJy)')) + op.add_column('target', sa.Column('psf_flux_error_j', sa.Float(), nullable=True, comment='Error in J band PSF flux (nJy)')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('target', 'psf_flux_error_j') + op.drop_column('target', 'psf_flux_error_y') + op.drop_column('target', 'psf_flux_error_z') + op.drop_column('target', 'psf_flux_error_i') + op.drop_column('target', 'psf_flux_error_r') + op.drop_column('target', 'psf_flux_error_g') + op.drop_column('target', 'psf_mag_error_j') + op.drop_column('target', 'psf_mag_error_y') + op.drop_column('target', 'psf_mag_error_z') + op.drop_column('target', 'psf_mag_error_i') + op.drop_column('target', 'psf_mag_error_r') + op.drop_column('target', 'psf_mag_error_g') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-142611_85c7fd1caf42_add_ob_code_in_target_table.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-142611_85c7fd1caf42_add_ob_code_in_target_table.py new file mode 100644 index 0000000..a5b5bf0 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-142611_85c7fd1caf42_add_ob_code_in_target_table.py @@ -0,0 +1,28 @@ +"""Add ob_code in target table. + +Revision ID: 85c7fd1caf42 +Revises: 92d3534d8bf8 +Create Date: 2022-12-06 14:26:11.366386 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '85c7fd1caf42' +down_revision = '92d3534d8bf8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('target', sa.Column('ob_code', sa.String(), nullable=True, comment='Identifer for a combination of a target, observing mode, and exposure time in a program.')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('target', 'ob_code') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-190448_9ef97473b712_make_nullable_false_for_ob_code_column.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-190448_9ef97473b712_make_nullable_false_for_ob_code_column.py new file mode 100644 index 0000000..0ef02da --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-190448_9ef97473b712_make_nullable_false_for_ob_code_column.py @@ -0,0 +1,34 @@ +"""Make nullable=False for ob_code column. + +Revision ID: 9ef97473b712 +Revises: 85c7fd1caf42 +Create Date: 2022-12-06 19:04:48.390219 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9ef97473b712' +down_revision = '85c7fd1caf42' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target', 'ob_code', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Identifer for a combination of a target, observing mode, and exposure time in a program.') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target', 'ob_code', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Identifer for a combination of a target, observing mode, and exposure time in a program.') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-193037_db972b098339_new_table_filter_name_is_created.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-193037_db972b098339_new_table_filter_name_is_created.py new file mode 100644 index 0000000..b538713 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-193037_db972b098339_new_table_filter_name_is_created.py @@ -0,0 +1,35 @@ +"""New table, filter_name, is created. + +Revision ID: db972b098339 +Revises: 9ef97473b712 +Create Date: 2022-12-06 19:30:37.646535 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'db972b098339' +down_revision = '9ef97473b712' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('filter_name', + sa.Column('filter_name', sa.String(), autoincrement=False, nullable=False, comment='Filter name (e.g., g_ps1)'), + sa.Column('filter_name_description', sa.String(), nullable=True, comment='Descriptino of the filter'), + sa.Column('created_at', sa.DateTime(), nullable=True, comment='Creation time [YYYY-MM-DDThh:mm:ss] (UTC)'), + sa.Column('updated_at', sa.DateTime(), nullable=True, comment='Update time [YYYY-MM-DDThh:mm:ss] (UTC)'), + sa.PrimaryKeyConstraint('filter_name'), + sa.UniqueConstraint('filter_name') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('filter_name') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-221328_d34d4aa2914a_foreign_key_relationships_to_filter_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-221328_d34d4aa2914a_foreign_key_relationships_to_filter_.py new file mode 100644 index 0000000..0f85988 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221206-221328_d34d4aa2914a_foreign_key_relationships_to_filter_.py @@ -0,0 +1,62 @@ +"""Foreign key relationships to filter_name created + +Revision ID: d34d4aa2914a +Revises: db972b098339 +Create Date: 2022-12-06 22:13:28.300403 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd34d4aa2914a' +down_revision = 'db972b098339' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('filter_name', 'filter_name_description', + existing_type=sa.VARCHAR(), + comment='Description of the filter', + existing_comment='Descriptino of the filter', + existing_nullable=True) + op.create_unique_constraint(None, 'filter_name', ['filter_name']) + op.create_foreign_key(None, 'fluxstd', 'filter_name', ['filter_j'], ['filter_name']) + op.create_foreign_key(None, 'fluxstd', 'filter_name', ['filter_z'], ['filter_name']) + op.create_foreign_key(None, 'fluxstd', 'filter_name', ['filter_g'], ['filter_name']) + op.create_foreign_key(None, 'fluxstd', 'filter_name', ['filter_r'], ['filter_name']) + op.create_foreign_key(None, 'fluxstd', 'filter_name', ['filter_y'], ['filter_name']) + op.create_foreign_key(None, 'fluxstd', 'filter_name', ['filter_i'], ['filter_name']) + op.create_foreign_key(None, 'target', 'filter_name', ['filter_j'], ['filter_name']) + op.create_foreign_key(None, 'target', 'filter_name', ['filter_z'], ['filter_name']) + op.create_foreign_key(None, 'target', 'filter_name', ['filter_y'], ['filter_name']) + op.create_foreign_key(None, 'target', 'filter_name', ['filter_i'], ['filter_name']) + op.create_foreign_key(None, 'target', 'filter_name', ['filter_g'], ['filter_name']) + op.create_foreign_key(None, 'target', 'filter_name', ['filter_r'], ['filter_name']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_constraint(None, 'fluxstd', type_='foreignkey') + op.drop_constraint(None, 'fluxstd', type_='foreignkey') + op.drop_constraint(None, 'fluxstd', type_='foreignkey') + op.drop_constraint(None, 'fluxstd', type_='foreignkey') + op.drop_constraint(None, 'fluxstd', type_='foreignkey') + op.drop_constraint(None, 'fluxstd', type_='foreignkey') + op.drop_constraint(None, 'filter_name', type_='unique') + op.alter_column('filter_name', 'filter_name_description', + existing_type=sa.VARCHAR(), + comment='Descriptino of the filter', + existing_comment='Description of the filter', + existing_nullable=True) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20221207-141054_c5f125bd0b9b_add_nullable_false_for_some_columns.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20221207-141054_c5f125bd0b9b_add_nullable_false_for_some_columns.py new file mode 100644 index 0000000..2ea0dc5 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20221207-141054_c5f125bd0b9b_add_nullable_false_for_some_columns.py @@ -0,0 +1,155 @@ +"""Add nullable=False for some columns + +Revision ID: c5f125bd0b9b +Revises: d34d4aa2914a +Create Date: 2022-12-07 14:10:54.033977 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'c5f125bd0b9b' +down_revision = 'd34d4aa2914a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('fluxstd', 'obj_id', + existing_type=sa.BIGINT(), + nullable=False, + existing_comment='Gaia EDR3 sourceid') + op.alter_column('fluxstd', 'ra', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='RA (ICRS, degree)') + op.alter_column('fluxstd', 'dec', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='Dec (ICRS, degree)') + op.alter_column('fluxstd', 'input_catalog_id', + existing_type=sa.INTEGER(), + nullable=False, + existing_comment='input_catalog_id from the input_catalog table') + op.alter_column('fluxstd', 'version', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Version string of the F-star selection') + op.alter_column('sky', 'obj_id', + existing_type=sa.BIGINT(), + nullable=False, + existing_comment='Object ID in the sky catalog') + op.alter_column('sky', 'ra', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='RA (ICRS, degree)') + op.alter_column('sky', 'dec', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='Dec (ICRS, degree)') + op.alter_column('sky', 'input_catalog_id', + existing_type=sa.INTEGER(), + nullable=False, + existing_comment='input_catalog_id from the input_catalog table') + op.alter_column('sky', 'version', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Version string of the sky position') + op.alter_column('target', 'obj_id', + existing_type=sa.BIGINT(), + nullable=False, + existing_comment='Object ID as specified by the observer at Phase 2 (can be same as the input_catalog_object_id)') + op.alter_column('target', 'ra', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='RA (ICRS, degree)') + op.alter_column('target', 'dec', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='Dec (ICRS, degree)') + op.alter_column('target', 'target_type_id', + existing_type=sa.INTEGER(), + comment='target type ID (default: 1 = SCIENCE)', + existing_nullable=True) + op.alter_column('target', 'input_catalog_id', + existing_type=sa.INTEGER(), + nullable=False, + existing_comment='Input catalog ID from the input_catalog table') + op.alter_column('target_type', 'target_type_name', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Name for the target type.') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target_type', 'target_type_name', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Name for the target type.') + op.alter_column('target', 'input_catalog_id', + existing_type=sa.INTEGER(), + nullable=True, + existing_comment='Input catalog ID from the input_catalog table') + op.alter_column('target', 'target_type_id', + existing_type=sa.INTEGER(), + comment=None, + existing_comment='target type ID (default: 1 = SCIENCE)', + existing_nullable=True) + op.alter_column('target', 'dec', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='Dec (ICRS, degree)') + op.alter_column('target', 'ra', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='RA (ICRS, degree)') + op.alter_column('target', 'obj_id', + existing_type=sa.BIGINT(), + nullable=True, + existing_comment='Object ID as specified by the observer at Phase 2 (can be same as the input_catalog_object_id)') + op.alter_column('sky', 'version', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Version string of the sky position') + op.alter_column('sky', 'input_catalog_id', + existing_type=sa.INTEGER(), + nullable=True, + existing_comment='input_catalog_id from the input_catalog table') + op.alter_column('sky', 'dec', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='Dec (ICRS, degree)') + op.alter_column('sky', 'ra', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='RA (ICRS, degree)') + op.alter_column('sky', 'obj_id', + existing_type=sa.BIGINT(), + nullable=True, + existing_comment='Object ID in the sky catalog') + op.alter_column('fluxstd', 'version', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Version string of the F-star selection') + op.alter_column('fluxstd', 'input_catalog_id', + existing_type=sa.INTEGER(), + nullable=True, + existing_comment='input_catalog_id from the input_catalog table') + op.alter_column('fluxstd', 'dec', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='Dec (ICRS, degree)') + op.alter_column('fluxstd', 'ra', + existing_type=postgresql.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='RA (ICRS, degree)') + op.alter_column('fluxstd', 'obj_id', + existing_type=sa.BIGINT(), + nullable=True, + existing_comment='Gaia EDR3 sourceid') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20230419-181440_790fae2257ac_add_a_unique_constraint_proposal_id_ob_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20230419-181440_790fae2257ac_add_a_unique_constraint_proposal_id_ob_.py new file mode 100644 index 0000000..6aeaeb0 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20230419-181440_790fae2257ac_add_a_unique_constraint_proposal_id_ob_.py @@ -0,0 +1,34 @@ +"""Add a unique constraint (proposal_id, ob_code) in the target table + +Revision ID: 790fae2257ac +Revises: c5f125bd0b9b +Create Date: 2023-04-19 18:14:40.791644 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '790fae2257ac' +down_revision = 'c5f125bd0b9b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_index('fluxstd_q3c_ang2ipix_idx', table_name='fluxstd') + # op.drop_index('sky_q3c_ang2ipix_idx', table_name='sky') + # op.drop_index('target_q3c_ang2ipix_idx', table_name='target') + op.create_unique_constraint('uq_proposal_id_ob_code', 'target', ['proposal_id', 'ob_code']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uq_proposal_id_ob_code', 'target', type_='unique') + # op.create_index('target_q3c_ang2ipix_idx', 'target', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('sky_q3c_ang2ipix_idx', 'sky', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('fluxstd_q3c_ang2ipix_idx', 'fluxstd', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20230420-150337_10e6d0a4610a_add_a_unique_constraint_by_obj_id_input_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20230420-150337_10e6d0a4610a_add_a_unique_constraint_by_obj_id_input_.py new file mode 100644 index 0000000..0636a05 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20230420-150337_10e6d0a4610a_add_a_unique_constraint_by_obj_id_input_.py @@ -0,0 +1,34 @@ +"""Add a unique constraint by (obj_id, input_catalog_id, version) for fluxstd + +Revision ID: 10e6d0a4610a +Revises: 790fae2257ac +Create Date: 2023-04-20 15:03:37.686829 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '10e6d0a4610a' +down_revision = '790fae2257ac' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_index('fluxstd_q3c_ang2ipix_idx', table_name='fluxstd') + op.create_unique_constraint('uq_obj_id_input_catalog_id_version', 'fluxstd', ['obj_id', 'input_catalog_id', 'version']) + # op.drop_index('sky_q3c_ang2ipix_idx', table_name='sky') + # op.drop_index('target_q3c_ang2ipix_idx', table_name='target') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.create_index('target_q3c_ang2ipix_idx', 'target', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('sky_q3c_ang2ipix_idx', 'sky', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + op.drop_constraint('uq_obj_id_input_catalog_id_version', 'fluxstd', type_='unique') + # op.create_index('fluxstd_q3c_ang2ipix_idx', 'fluxstd', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20230420-163804_8ac6065d2225_add_columns_of_stellar_parameters_teff_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20230420-163804_8ac6065d2225_add_columns_of_stellar_parameters_teff_.py new file mode 100644 index 0000000..76cb688 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20230420-163804_8ac6065d2225_add_columns_of_stellar_parameters_teff_.py @@ -0,0 +1,44 @@ +"""Add columns of stellar parameters (teff and logg + confidence intervals) in the fluxstd table + +Revision ID: 8ac6065d2225 +Revises: 10e6d0a4610a +Create Date: 2023-04-20 16:38:04.417493 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8ac6065d2225' +down_revision = '10e6d0a4610a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('fluxstd', sa.Column('teff_brutus', sa.Float(), nullable=True, comment='Effective temperature from Brutus code [K]')) + op.add_column('fluxstd', sa.Column('teff_brutus_low', sa.Float(), nullable=True, comment='Lower confidence level (16%) of effective temperature from Brutus code [K]')) + op.add_column('fluxstd', sa.Column('teff_brutus_high', sa.Float(), nullable=True, comment='Upper confidence level (84%) of effective temperature from Brutus code [K]')) + op.add_column('fluxstd', sa.Column('logg_brutus', sa.Float(), nullable=True, comment='Surface gravity from Brutus code [log cgs]')) + op.add_column('fluxstd', sa.Column('logg_brutus_low', sa.Float(), nullable=True, comment='Lower confidence level (16%) of surface gravity from Brutus code [log cgs]')) + op.add_column('fluxstd', sa.Column('logg_brutus_high', sa.Float(), nullable=True, comment='Upper confidence level (84%) of surface gravity from Brutus code [log cgs]')) + # op.drop_index('fluxstd_q3c_ang2ipix_idx', table_name='fluxstd') + # op.drop_index('sky_q3c_ang2ipix_idx', table_name='sky') + # op.drop_index('target_q3c_ang2ipix_idx', table_name='target') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.create_index('target_q3c_ang2ipix_idx', 'target', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('sky_q3c_ang2ipix_idx', 'sky', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('fluxstd_q3c_ang2ipix_idx', 'fluxstd', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + op.drop_column('fluxstd', 'logg_brutus_high') + op.drop_column('fluxstd', 'logg_brutus_low') + op.drop_column('fluxstd', 'logg_brutus') + op.drop_column('fluxstd', 'teff_brutus_high') + op.drop_column('fluxstd', 'teff_brutus_low') + op.drop_column('fluxstd', 'teff_brutus') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20230421-091340_6aec47e0f339_add_an_index_to_version_in_fluxstd.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20230421-091340_6aec47e0f339_add_an_index_to_version_in_fluxstd.py new file mode 100644 index 0000000..123a63d --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20230421-091340_6aec47e0f339_add_an_index_to_version_in_fluxstd.py @@ -0,0 +1,34 @@ +"""Add an index to version in fluxstd + +Revision ID: 6aec47e0f339 +Revises: 8ac6065d2225 +Create Date: 2023-04-21 09:13:40.822777 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6aec47e0f339' +down_revision = '8ac6065d2225' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_index('fluxstd_q3c_ang2ipix_idx', table_name='fluxstd') + op.create_index(op.f('ix_fluxstd_version'), 'fluxstd', ['version'], unique=False) + # op.drop_index('sky_q3c_ang2ipix_idx', table_name='sky') + # op.drop_index('target_q3c_ang2ipix_idx', table_name='target') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.create_index('target_q3c_ang2ipix_idx', 'target', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('sky_q3c_ang2ipix_idx', 'sky', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + op.drop_index(op.f('ix_fluxstd_version'), table_name='fluxstd') + # op.create_index('fluxstd_q3c_ang2ipix_idx', 'fluxstd', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20230504-175323_c987ab91b094_add_indexes_to_input_catalog_id_and_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20230504-175323_c987ab91b094_add_indexes_to_input_catalog_id_and_.py new file mode 100644 index 0000000..ec15b91 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20230504-175323_c987ab91b094_add_indexes_to_input_catalog_id_and_.py @@ -0,0 +1,36 @@ +"""Add indexes to input_catalog_id and version + +Revision ID: c987ab91b094 +Revises: 6aec47e0f339 +Create Date: 2023-05-04 17:53:23.315181 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c987ab91b094' +down_revision = '6aec47e0f339' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.drop_index('fluxstd_q3c_ang2ipix_idx', table_name='fluxstd') + # op.drop_index('sky_q3c_ang2ipix_idx', table_name='sky') + op.create_index(op.f('ix_sky_input_catalog_id'), 'sky', ['input_catalog_id'], unique=False) + op.create_index(op.f('ix_sky_version'), 'sky', ['version'], unique=False) + # op.drop_index('target_q3c_ang2ipix_idx', table_name='target') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.create_index('target_q3c_ang2ipix_idx', 'target', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + op.drop_index(op.f('ix_sky_version'), table_name='sky') + op.drop_index(op.f('ix_sky_input_catalog_id'), table_name='sky') + # op.create_index('sky_q3c_ang2ipix_idx', 'sky', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # op.create_index('fluxstd_q3c_ang2ipix_idx', 'fluxstd', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20230505-181644_099e9069f2b7_test_q3c_indexes.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20230505-181644_099e9069f2b7_test_q3c_indexes.py new file mode 100644 index 0000000..e34e4bd --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20230505-181644_099e9069f2b7_test_q3c_indexes.py @@ -0,0 +1,28 @@ +"""Test q3c indexes + +Revision ID: 099e9069f2b7 +Revises: c987ab91b094 +Create Date: 2023-05-05 18:16:44.225953 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '099e9069f2b7' +down_revision = 'c987ab91b094' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240202-151144_71eba81781d9_add_allocated_time_in_total_lr_and_mr.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240202-151144_71eba81781d9_add_allocated_time_in_total_lr_and_mr.py new file mode 100644 index 0000000..10ecedf --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240202-151144_71eba81781d9_add_allocated_time_in_total_lr_and_mr.py @@ -0,0 +1,53 @@ +"""Add allocated_time in total, lr, and mr + +Revision ID: 71eba81781d9 +Revises: 099e9069f2b7 +Create Date: 2024-02-02 15:11:44.124658 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "71eba81781d9" +down_revision = "099e9069f2b7" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "proposal", "allocated_time", new_column_name="allocated_time_total" + ) + op.add_column( + "proposal", + sa.Column( + "allocated_time_lr", + sa.Float(), + nullable=True, + comment="Total fiberhours for the low-resolution mode allocated by TAC (hour)", + ), + ) + op.add_column( + "proposal", + sa.Column( + "allocated_time_mr", + sa.Float(), + nullable=True, + comment="Total fiberhours for the medium-resolution mode allocated by TAC (hour)", + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "proposal", "allocated_time_total", new_column_name="allocated_time" + ) + op.drop_column("proposal", "allocated_time_mr") + op.drop_column("proposal", "allocated_time_lr") + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240202-153755_320fc1f7de3b_add_upload_id_to_input_catalog_table.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240202-153755_320fc1f7de3b_add_upload_id_to_input_catalog_table.py new file mode 100644 index 0000000..d9e4f33 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240202-153755_320fc1f7de3b_add_upload_id_to_input_catalog_table.py @@ -0,0 +1,28 @@ +"""Add upload_id to input_catalog table + +Revision ID: 320fc1f7de3b +Revises: 71eba81781d9 +Create Date: 2024-02-02 15:37:55.782110 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '320fc1f7de3b' +down_revision = '71eba81781d9' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('input_catalog', sa.Column('upload_id', sa.String(length=16), nullable=True, comment='A 8-bit hex string (16 characters) assigned at the submission of the target list (default: empty string)')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('input_catalog', 'upload_id') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240404-173437_d024af8c52f2_add_gaia_parameters_in_the_fluxstd_table.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240404-173437_d024af8c52f2_add_gaia_parameters_in_the_fluxstd_table.py new file mode 100644 index 0000000..cd3e0ec --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240404-173437_d024af8c52f2_add_gaia_parameters_in_the_fluxstd_table.py @@ -0,0 +1,67 @@ +"""Add gaia parameters in the fluxstd table + +Revision ID: d024af8c52f2 +Revises: 320fc1f7de3b +Create Date: 2024-04-04 17:34:37.233549 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d024af8c52f2" +down_revision = "320fc1f7de3b" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "fluxstd", + sa.Column( + "teff_gspphot", + sa.Float(), + nullable=True, + comment="Effective temperature inferred by GSP-phot Aeneas [K]", + ), + ) + op.add_column( + "fluxstd", + sa.Column( + "teff_gspphot_lower", + sa.Float(), + nullable=True, + comment="Lower confidence level (16%) of effective temperature inferred by GSP-phot Aeneas [K]", + ), + ) + op.add_column( + "fluxstd", + sa.Column( + "teff_gspphot_upper", + sa.Float(), + nullable=True, + comment="Upper confidence level (84%) of effective temperature inferred by GSP-phot Aeneas [K]", + ), + ) + op.add_column( + "fluxstd", + sa.Column( + "is_fstar_gaia", + sa.Boolean(), + default=False, + comment="Flag for F-star from Gaia (Teff=6000-7500K if True)", + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("fluxstd", "is_fstar_gaia") + op.drop_column("fluxstd", "teff_gspphot_upper") + op.drop_column("fluxstd", "teff_gspphot_lower") + op.drop_column("fluxstd", "teff_gspphot") + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240408-092814_b010233c946e_fix_typo_in_filter_relationship_in_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240408-092814_b010233c946e_fix_typo_in_filter_relationship_in_.py new file mode 100644 index 0000000..e225bf3 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240408-092814_b010233c946e_fix_typo_in_filter_relationship_in_.py @@ -0,0 +1,28 @@ +"""Fix typo in filter relationship in fluxstd + +Revision ID: b010233c946e +Revises: d024af8c52f2 +Create Date: 2024-04-08 09:28:14.313541 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b010233c946e' +down_revision = 'd024af8c52f2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240415-102112_eeb9bd9b8d91_autoincrement_input_catalog_id.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240415-102112_eeb9bd9b8d91_autoincrement_input_catalog_id.py new file mode 100644 index 0000000..a6b102c --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240415-102112_eeb9bd9b8d91_autoincrement_input_catalog_id.py @@ -0,0 +1,157 @@ +"""autoincrement input_catalog_id + +Revision ID: eeb9bd9b8d91 +Revises: b010233c946e +Create Date: 2024-04-15 10:21:12.653189 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "eeb9bd9b8d91" +down_revision: Union[str, None] = "b010233c946e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +# generated by the help of Copilot. +def upgrade() -> None: + # Start a transaction block + op.execute("BEGIN;") + + # Drop the foreign key constraints in the other tables + op.execute( + """ + ALTER TABLE sky DROP CONSTRAINT sky_input_catalog_id_fkey; + ALTER TABLE fluxstd DROP CONSTRAINT fluxstd_input_catalog_id_fkey; + ALTER TABLE target DROP CONSTRAINT target_input_catalog_id_fkey; + ALTER TABLE cluster DROP CONSTRAINT cluster_input_catalog_id_fkey; + """ + ) + + # Rename the existing input_catalog_id column to tmp_input_catalog_id + op.execute( + """ + ALTER TABLE input_catalog RENAME COLUMN input_catalog_id TO tmp_input_catalog_id; + """ + ) + + # Create a new input_catalog_id column with the Identity option + op.execute( + """ + ALTER TABLE input_catalog ADD COLUMN input_catalog_id INT GENERATED BY DEFAULT AS IDENTITY; + """ + ) + + # Set the starting value of the sequence associated with the input_catalog_id column to 10000 + op.execute( + """ + ALTER SEQUENCE input_catalog_input_catalog_id_seq RESTART WITH 10000 MAXVALUE 89999; + """ + ) + + # Copy the values from tmp_input_catalog_id to input_catalog_id + op.execute( + """ + UPDATE input_catalog SET input_catalog_id = tmp_input_catalog_id; + """ + ) + + # Drop the primary key constraint on tmp_input_catalog_id and add a primary key constraint on input_catalog_id in a single step + op.execute( + """ + ALTER TABLE input_catalog + DROP CONSTRAINT input_catalog_pkey, + ADD PRIMARY KEY (input_catalog_id); + """ + ) + + # Delete the tmp_input_catalog_id column + op.execute( + """ + ALTER TABLE input_catalog DROP COLUMN tmp_input_catalog_id; + """ + ) + + # Add the foreign key constraints back to the other tables + op.execute( + """ + ALTER TABLE sky ADD CONSTRAINT sky_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + ALTER TABLE fluxstd ADD CONSTRAINT fluxstd_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + ALTER TABLE target ADD CONSTRAINT target_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + ALTER TABLE cluster ADD CONSTRAINT cluster_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + """ + ) + + # Commit the transaction block + op.execute("COMMIT;") + + +def downgrade() -> None: + # Start a transaction block + op.execute("BEGIN;") + + # Drop the foreign key constraints in the other tables + op.execute( + """ + ALTER TABLE sky DROP CONSTRAINT sky_input_catalog_id_fkey; + ALTER TABLE fluxstd DROP CONSTRAINT fluxstd_input_catalog_id_fkey; + ALTER TABLE target DROP CONSTRAINT target_input_catalog_id_fkey; + ALTER TABLE cluster DROP CONSTRAINT cluster_input_catalog_id_fkey; + """ + ) + + # Rename the existing input_catalog_id column to tmp_input_catalog_id + op.execute( + """ + ALTER TABLE input_catalog RENAME COLUMN input_catalog_id TO tmp_input_catalog_id; + """ + ) + + # Create a new input_catalog_id column without the Identity option + op.execute( + """ + ALTER TABLE input_catalog ADD COLUMN input_catalog_id INT; + """ + ) + + # Copy the values from tmp_input_catalog_id to input_catalog_id + op.execute( + """ + UPDATE input_catalog SET input_catalog_id = tmp_input_catalog_id; + """ + ) + + # Drop the primary key constraint on tmp_input_catalog_id and add a primary key constraint on input_catalog_id in a single step + op.execute( + """ + ALTER TABLE input_catalog + DROP CONSTRAINT input_catalog_pkey, + ADD PRIMARY KEY (input_catalog_id); + """ + ) + + # Delete the tmp_input_catalog_id column + op.execute( + """ + ALTER TABLE input_catalog DROP COLUMN tmp_input_catalog_id; + """ + ) + + # Add the foreign key constraints back to the other tables + op.execute( + """ + ALTER TABLE sky ADD CONSTRAINT sky_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + ALTER TABLE fluxstd ADD CONSTRAINT fluxstd_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + ALTER TABLE target ADD CONSTRAINT target_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + ALTER TABLE cluster ADD CONSTRAINT cluster_input_catalog_id_fkey FOREIGN KEY (input_catalog_id) REFERENCES input_catalog (input_catalog_id); + """ + ) + + # Commit the transaction block + op.execute("COMMIT;") diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240416-104950_1f393a55b6d8_remove_unique_constraint_from_input_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240416-104950_1f393a55b6d8_remove_unique_constraint_from_input_.py new file mode 100644 index 0000000..a003384 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240416-104950_1f393a55b6d8_remove_unique_constraint_from_input_.py @@ -0,0 +1,66 @@ +"""remove unique constraint from input_catalog_name + +Revision ID: 1f393a55b6d8 +Revises: eeb9bd9b8d91 +Create Date: 2024-04-16 10:49:50.036660 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "1f393a55b6d8" +down_revision: Union[str, None] = "eeb9bd9b8d91" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "input_catalog", + "input_catalog_id", + existing_type=sa.INTEGER(), + server_default=sa.Identity( + always=False, start=10000, maxvalue=89999, cycle=False + ), + comment="Unique identifier for input catalogs", + existing_nullable=False, + autoincrement=True, + ) + op.drop_constraint( + "input_catalog_input_catalog_name_key", "input_catalog", type_="unique" + ) + op.create_unique_constraint(None, "input_catalog", ["input_catalog_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "input_catalog", type_="unique") + op.create_unique_constraint( + "input_catalog_input_catalog_name_key", "input_catalog", ["input_catalog_name"] + ) + op.alter_column( + "input_catalog", + "input_catalog_id", + existing_type=sa.INTEGER(), + server_default=sa.Identity( + always=False, + start=1, + increment=1, + minvalue=1, + maxvalue=89999, + cycle=False, + cache=1, + ), + comment=None, + existing_comment="Unique identifier for input catalogs", + existing_nullable=False, + autoincrement=True, + ) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240416-105545_065b74905fb6_update_unique_constraint_for_the_target_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240416-105545_065b74905fb6_update_unique_constraint_for_the_target_.py new file mode 100644 index 0000000..6ab6412 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240416-105545_065b74905fb6_update_unique_constraint_for_the_target_.py @@ -0,0 +1,39 @@ +"""Update unique constraint for the target table + +Revision ID: 065b74905fb6 +Revises: 1f393a55b6d8 +Create Date: 2024-04-16 10:55:45.817899 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "065b74905fb6" +down_revision = "1f393a55b6d8" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uq_proposal_id_ob_code", "target", type_="unique") + op.create_unique_constraint( + "uq_proposal_id_ob_code_input_catalog_id_obj_id", + "target", + ["proposal_id", "ob_code", "input_catalog_id", "obj_id"], + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "uq_proposal_id_ob_code_input_catalog_id_obj_id", "target", type_="unique" + ) + op.create_unique_constraint( + "uq_proposal_id_ob_code", "target", ["proposal_id", "ob_code"] + ) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240417-100959_c636e8e2ecc0_add_unique_constraint_on_the_sky_table.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240417-100959_c636e8e2ecc0_add_unique_constraint_on_the_sky_table.py new file mode 100644 index 0000000..7cfe4bf --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240417-100959_c636e8e2ecc0_add_unique_constraint_on_the_sky_table.py @@ -0,0 +1,43 @@ +"""add unique constraint on the sky table + +Revision ID: c636e8e2ecc0 +Revises: 065b74905fb6 +Create Date: 2024-04-17 10:09:59.291914 + +Note: +Before upgrading the database, I run the following query to avoid the violation of the unique constaint: +``` +UPDATE sky SET obj_id = sky_id WHERE version = '20220915'; +``` +This is already implemented in the integrated code when processing sky objects. + +- [ ] production db +- [x] e2e db + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c636e8e2ecc0" +down_revision = "065b74905fb6" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint( + "sky_obj_id_input_catalog_id_version_key", + "sky", + ["obj_id", "input_catalog_id", "version"], + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("sky_obj_id_input_catalog_id_version_key", "sky", type_="unique") + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240422-150510_0049efa2957b_get_created_at_updated_at_automatically.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240422-150510_0049efa2957b_get_created_at_updated_at_automatically.py new file mode 100644 index 0000000..92e8401 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240422-150510_0049efa2957b_get_created_at_updated_at_automatically.py @@ -0,0 +1,272 @@ +"""get created_at/updated_at automatically + +Revision ID: 0049efa2957b +Revises: c636e8e2ecc0 +Create Date: 2024-04-22 15:05:10.412259 + +""" + +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "0049efa2957b" +down_revision = "c636e8e2ecc0" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "filter_name", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_comment="Creation time [YYYY-MM-DDThh:mm:ss] (UTC)", + existing_nullable=True, + ) + op.alter_column( + "filter_name", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_comment="Update time [YYYY-MM-DDThh:mm:ss] (UTC)", + existing_nullable=True, + ) + op.alter_column( + "fluxstd", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "fluxstd", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "input_catalog", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "input_catalog", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "proposal", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_comment="Creation time [YYYY-MM-DDThh:mm:ss] (UTC or HST?)", + existing_nullable=True, + ) + op.alter_column( + "proposal", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_comment="Update time [YYYY-MM-DDThh:mm:ss] (UTC or HST?)", + existing_nullable=True, + ) + op.alter_column( + "proposal_category", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_comment="Creation time", + existing_nullable=True, + ) + op.alter_column( + "proposal_category", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_comment="Update time", + existing_nullable=True, + ) + op.alter_column( + "sky", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "sky", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "target", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "target", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "target_type", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "target_type", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "target_type", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "target_type", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "target", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "target", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "sky", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "sky", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "proposal_category", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="Update time", + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "proposal_category", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="Creation time", + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "proposal", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="Update time [YYYY-MM-DDThh:mm:ss] (UTC or HST?)", + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "proposal", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="Creation time [YYYY-MM-DDThh:mm:ss] (UTC or HST?)", + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "input_catalog", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "input_catalog", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "fluxstd", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "fluxstd", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "filter_name", + "updated_at", + existing_type=postgresql.TIMESTAMP(), + comment="Update time [YYYY-MM-DDThh:mm:ss] (UTC)", + existing_comment="The date and time in UTC when the record was last updated", + existing_nullable=True, + ) + op.alter_column( + "filter_name", + "created_at", + existing_type=postgresql.TIMESTAMP(), + comment="Creation time [YYYY-MM-DDThh:mm:ss] (UTC)", + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240422-161159_1d0bd9825071_refine_default_values_and_nullable_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240422-161159_1d0bd9825071_refine_default_values_and_nullable_.py new file mode 100644 index 0000000..0874dc8 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240422-161159_1d0bd9825071_refine_default_values_and_nullable_.py @@ -0,0 +1,84 @@ +"""refine default values and nullable conditions + +Revision ID: 1d0bd9825071 +Revises: 0049efa2957b +Create Date: 2024-04-22 16:11:59.655450 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1d0bd9825071' +down_revision = '0049efa2957b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('input_catalog', 'input_catalog_name', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Name of the input catalog (e.g., Gaia DR2, HSC-SSP PDR3, etc.)') + op.alter_column('proposal', 'group_id', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Group ID in STARS (e.g., o21195?)', + autoincrement=False) + op.alter_column('proposal', 'pi_last_name', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment="PI's last name") + op.alter_column('proposal', 'rank', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='TAC score') + op.alter_column('proposal', 'grade', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='TAC grade (A/B/C/F in the case of HSC queue)') + op.alter_column('proposal_category', 'proposal_category_name', + existing_type=sa.VARCHAR(), + nullable=False, + existing_comment='Proposal category name (e.g., Openuse, Keck, Gemini, and UH)') + op.alter_column('target', 'effective_exptime', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='Requested effective exposure time (s)') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target', 'effective_exptime', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='Requested effective exposure time (s)') + op.alter_column('proposal_category', 'proposal_category_name', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Proposal category name (e.g., Openuse, Keck, Gemini, and UH)') + op.alter_column('proposal', 'grade', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='TAC grade (A/B/C/F in the case of HSC queue)') + op.alter_column('proposal', 'rank', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='TAC score') + op.alter_column('proposal', 'pi_last_name', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment="PI's last name") + op.alter_column('proposal', 'group_id', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Group ID in STARS (e.g., o21195?)', + autoincrement=False) + op.alter_column('input_catalog', 'input_catalog_name', + existing_type=sa.VARCHAR(), + nullable=True, + existing_comment='Name of the input catalog (e.g., Gaia DR2, HSC-SSP PDR3, etc.)') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240501-170926_f8bacfde2cef_add_active_flag_to_the_input_catalog_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240501-170926_f8bacfde2cef_add_active_flag_to_the_input_catalog_.py new file mode 100644 index 0000000..3da17a7 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240501-170926_f8bacfde2cef_add_active_flag_to_the_input_catalog_.py @@ -0,0 +1,43 @@ +"""add active flag to the input_catalog table + +Revision ID: f8bacfde2cef +Revises: 1d0bd9825071 +Create Date: 2024-05-01 17:09:26.387829 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f8bacfde2cef" +down_revision = "1d0bd9825071" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "input_catalog", + sa.Column( + "active", + sa.Boolean(), + nullable=True, + comment="Flag to indicate if the input catalog is active (default: True)", + ), + ) + op.execute( + """ + UPDATE input_catalog + SET active = TRUE + """ + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("input_catalog", "active") + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240502-112946_8f188c3cb652_set_default_to_created_at.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240502-112946_8f188c3cb652_set_default_to_created_at.py new file mode 100644 index 0000000..9006952 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240502-112946_8f188c3cb652_set_default_to_created_at.py @@ -0,0 +1,172 @@ +"""set default to created_at + +Revision ID: 8f188c3cb652 +Revises: f8bacfde2cef +Create Date: 2024-05-02 11:29:46.318970 + +""" + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "8f188c3cb652" +down_revision = "f8bacfde2cef" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "cluster", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="UTC", + existing_nullable=True, + ) + op.alter_column( + "filter_name", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "fluxstd", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "input_catalog", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "proposal", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "proposal_category", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "sky", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "target", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "target_type", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "target_type", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "target", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "sky", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "proposal_category", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "proposal", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "input_catalog", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "fluxstd", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "filter_name", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="The date and time in UTC when the record was created", + existing_nullable=True, + ) + op.alter_column( + "cluster", + "created_at", + existing_type=postgresql.TIMESTAMP(), + server_default=None, + existing_comment="UTC", + existing_nullable=True, + ) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240502-155205_368dce443754_unique_constraints_on_target.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240502-155205_368dce443754_unique_constraints_on_target.py new file mode 100644 index 0000000..751dde0 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240502-155205_368dce443754_unique_constraints_on_target.py @@ -0,0 +1,53 @@ +"""unique constraints on target + +Revision ID: 368dce443754 +Revises: 8f188c3cb652 +Create Date: 2024-05-02 15:52:05.728826 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "368dce443754" +down_revision = "8f188c3cb652" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "uq_proposal_id_ob_code_input_catalog_id_obj_id", "target", type_="unique" + ) + op.create_unique_constraint( + "target_propid_obcode_catid_objid_resolution_key", + "target", + [ + "proposal_id", + "ob_code", + "input_catalog_id", + "obj_id", + "is_medium_resolution", + ], + ) + op.create_unique_constraint( + "target_propid_obcode_key", "target", ["proposal_id", "ob_code"] + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("target_propid_obcode_key", "target", type_="unique") + op.drop_constraint( + "target_propid_obcode_catid_objid_resolution_key", "target", type_="unique" + ) + op.create_unique_constraint( + "uq_proposal_id_ob_code_input_catalog_id_obj_id", + "target", + ["proposal_id", "ob_code", "input_catalog_id", "obj_id"], + ) + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240524-145050_f630d28235eb_add_single_exptime_to_the_target_table.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240524-145050_f630d28235eb_add_single_exptime_to_the_target_table.py new file mode 100644 index 0000000..1d268f2 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240524-145050_f630d28235eb_add_single_exptime_to_the_target_table.py @@ -0,0 +1,28 @@ +"""Add single_exptime to the target table + +Revision ID: f630d28235eb +Revises: 368dce443754 +Create Date: 2024-05-24 14:50:50.589144 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f630d28235eb' +down_revision = '368dce443754' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('target', sa.Column('single_exptime', sa.Float(), nullable=True, comment='Individual exposure time (s)')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('target', 'single_exptime') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240524-145817_1027c2966c63_set_single_exptime_un_nullable.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240524-145817_1027c2966c63_set_single_exptime_un_nullable.py new file mode 100644 index 0000000..4f75e40 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240524-145817_1027c2966c63_set_single_exptime_un_nullable.py @@ -0,0 +1,34 @@ +"""set single_exptime un-nullable + +Revision ID: 1027c2966c63 +Revises: f630d28235eb +Create Date: 2024-05-24 14:58:17.757023 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1027c2966c63' +down_revision = 'f630d28235eb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target', 'single_exptime', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=False, + existing_comment='Individual exposure time (s)') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target', 'single_exptime', + existing_type=sa.DOUBLE_PRECISION(precision=53), + nullable=True, + existing_comment='Individual exposure time (s)') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240606-145135_63584f7b182a_add_arm_info.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240606-145135_63584f7b182a_add_arm_info.py new file mode 100644 index 0000000..86f1b8c --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240606-145135_63584f7b182a_add_arm_info.py @@ -0,0 +1,39 @@ +"""add arm info + +Revision ID: 63584f7b182a +Revises: 1027c2966c63 +Create Date: 2024-06-06 14:51:35.709957 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '63584f7b182a' +down_revision = '1027c2966c63' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('pfs_arm', + sa.Column('name', sa.String(), nullable=False, comment="Arm name (e.g., 'b', 'r', 'n', and 'm')"), + sa.Column('description', sa.String(), nullable=True, comment='Arm description'), + sa.Column('created_at', sa.DateTime(), server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), nullable=True, comment='The date and time in UTC when the record was created'), + sa.Column('updated_at', sa.DateTime(), nullable=True, comment='The date and time in UTC when the record was last updated'), + sa.PrimaryKeyConstraint('name'), + sa.UniqueConstraint('name') + ) + op.add_column('target', sa.Column('qa_reference_arm', sa.String(), nullable=True, comment="Reference arm to evaluate effective exposure time ['b'|'r'|'n'|'m']")) + op.create_foreign_key(None, 'target', 'pfs_arm', ['qa_reference_arm'], ['name']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'target', type_='foreignkey') + op.drop_column('target', 'qa_reference_arm') + op.drop_table('pfs_arm') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240606-172208_bc79254cc64b_pfs_arm_name_to_be_unique.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240606-172208_bc79254cc64b_pfs_arm_name_to_be_unique.py new file mode 100644 index 0000000..e314fcc --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240606-172208_bc79254cc64b_pfs_arm_name_to_be_unique.py @@ -0,0 +1,28 @@ +"""pfs_arm.name to be unique + +Revision ID: bc79254cc64b +Revises: 63584f7b182a +Create Date: 2024-06-06 17:22:08.628641 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bc79254cc64b' +down_revision = '63584f7b182a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, 'pfs_arm', ['name']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'pfs_arm', type_='unique') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20240624-165136_f07c300ae87f_add_queue_classical_too_flags.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20240624-165136_f07c300ae87f_add_queue_classical_too_flags.py new file mode 100644 index 0000000..dac3378 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20240624-165136_f07c300ae87f_add_queue_classical_too_flags.py @@ -0,0 +1,40 @@ +"""add queue-classical-too flags + +Revision ID: f07c300ae87f +Revises: bc79254cc64b +Create Date: 2024-06-24 16:51:36.392229 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f07c300ae87f' +down_revision = 'bc79254cc64b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('input_catalog', sa.Column('is_classical', sa.Boolean(), nullable=True, comment='True if the classical mode is requested')) + op.add_column('proposal', sa.Column('is_too', sa.Boolean(), nullable=True, comment='True when the proposal is ToO')) + op.alter_column('target', 'qa_reference_arm', + existing_type=sa.VARCHAR(), + comment="Reference arm to evaluate effective exposure time ('b', 'r', 'n', 'm')", + existing_comment="Reference arm to evaluate effective exposure time ['b'|'r'|'n'|'m']", + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('target', 'qa_reference_arm', + existing_type=sa.VARCHAR(), + comment="Reference arm to evaluate effective exposure time ['b'|'r'|'n'|'m']", + existing_comment="Reference arm to evaluate effective exposure time ('b', 'r', 'n', 'm')", + existing_nullable=True) + op.drop_column('proposal', 'is_too') + op.drop_column('input_catalog', 'is_classical') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20241024-120338_b68482e38606_add_user_pointing_table.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20241024-120338_b68482e38606_add_user_pointing_table.py new file mode 100644 index 0000000..8fab0cb --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20241024-120338_b68482e38606_add_user_pointing_table.py @@ -0,0 +1,109 @@ +"""add user_pointing table + +Revision ID: b68482e38606 +Revises: f07c300ae87f +Create Date: 2024-10-24 12:03:38.150098 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b68482e38606" +down_revision = "f07c300ae87f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user_pointing", + sa.Column( + "user_pointing_id", + sa.BigInteger(), + nullable=False, + comment="Unique identifier for each user-defined pointing (autoincremented primary key)", + ), + sa.Column( + "ppc_code", + sa.String(), + nullable=False, + comment="String identifier of the pointing set either by the uploader or user", + ), + sa.Column( + "ppc_ra", + sa.Float(), + nullable=False, + comment="RA of the pointing center (ICRS, degree)", + ), + sa.Column( + "ppc_dec", + sa.Float(), + nullable=False, + comment="Dec of the pointing center (ICRS, degree)", + ), + sa.Column( + "ppc_pa", + sa.Float(), + nullable=False, + comment="Position angle of the pointing center (degree)", + ), + sa.Column( + "ppc_resolution", + sa.Enum("L", "M", name="resolutionmode"), + nullable=False, + comment="Resolution mode of the pointing ('L' or 'M')", + ), + sa.Column( + "ppc_priority", + sa.Float(), + nullable=False, + comment="Priority of the pointing calculated by the uploader", + ), + sa.Column( + "input_catalog_id", + sa.Integer(), + nullable=False, + comment="Input catalog ID from the input_catalog table", + ), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), + nullable=True, + comment="The date and time in UTC when the record was created", + ), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=True, + comment="The date and time in UTC when the record was last updated", + ), + sa.ForeignKeyConstraint( + ["input_catalog_id"], + ["input_catalog.input_catalog_id"], + ), + sa.PrimaryKeyConstraint("user_pointing_id"), + ) + op.add_column( + "input_catalog", + sa.Column( + "is_user_pointing", + sa.Boolean(), + nullable=True, + comment="True if user-defined pointings are provided", + ), + ) + # op.drop_index('target_q3c_ang2ipix_idx1', table_name='target') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # op.create_index('target_q3c_ang2ipix_idx1', 'target', [sa.text('q3c_ang2ipix(ra, "dec")')], unique=False) + op.drop_column("input_catalog", "is_user_pointing") + op.drop_table("user_pointing") + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20241220-145409_0b72e62efd44_add_partner_column_in_the_proposal_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20241220-145409_0b72e62efd44_add_partner_column_in_the_proposal_.py new file mode 100644 index 0000000..862fb74 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20241220-145409_0b72e62efd44_add_partner_column_in_the_proposal_.py @@ -0,0 +1,41 @@ +"""add partner column in the proposal table and partner table + +Revision ID: 0b72e62efd44 +Revises: b68482e38606 +Create Date: 2024-12-20 14:54:09.851011 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0b72e62efd44' +down_revision = 'b68482e38606' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('partner', + sa.Column('partner_id', sa.Integer(), autoincrement=False, nullable=False, comment='Unique identifier of the partner'), + sa.Column('partner_name', sa.String(), nullable=False, comment='Name of the partner (e.g., subaru, gemini, keck, and uh)'), + sa.Column('partner_description', sa.String(), nullable=True, comment='Description of the partner'), + sa.Column('created_at', sa.DateTime(), server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), nullable=True, comment='The date and time in UTC when the record was created'), + sa.Column('updated_at', sa.DateTime(), nullable=True, comment='The date and time in UTC when the record was last updated'), + sa.PrimaryKeyConstraint('partner_id'), + sa.UniqueConstraint('partner_id'), + sa.UniqueConstraint('partner_name') + ) + op.add_column('proposal', sa.Column('partner_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'proposal', 'partner', ['partner_id'], ['partner_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'proposal', type_='foreignkey') + op.drop_column('proposal', 'partner_id') + op.drop_table('partner') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20250331-121154_5b40ed8dcd48_create_index_on_proposal_grade.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20250331-121154_5b40ed8dcd48_create_index_on_proposal_grade.py new file mode 100644 index 0000000..c4e175b --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20250331-121154_5b40ed8dcd48_create_index_on_proposal_grade.py @@ -0,0 +1,83 @@ +"""create index on proposal.grade + +Revision ID: 5b40ed8dcd48 +Revises: 0b72e62efd44 +Create Date: 2025-03-31 12:11:54.201748 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "5b40ed8dcd48" +down_revision = "0b72e62efd44" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, "partner", ["partner_id"]) + op.alter_column( + "proposal", + "proposal_id", + existing_type=sa.VARCHAR(), + comment="Unique identifier for proposal (e.g, S21B-OT06)", + existing_comment="Unique identifier for proposal (e.g, S21B-OT06?)", + existing_nullable=False, + autoincrement=False, + ) + op.alter_column( + "proposal", + "group_id", + existing_type=sa.VARCHAR(), + comment="Group ID in STARS (e.g., o21195)", + existing_comment="Group ID in STARS (e.g., o21195?)", + existing_nullable=False, + autoincrement=False, + ) + op.alter_column( + "proposal", + "grade", + existing_type=sa.VARCHAR(), + comment="TAC grade (A/B/C/F and N/A)", + existing_comment="TAC grade (A/B/C/F in the case of HSC queue)", + existing_nullable=False, + ) + op.create_index("idx_proposal_grade", "proposal", ["grade"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("idx_proposal_grade", table_name="proposal") + op.alter_column( + "proposal", + "grade", + existing_type=sa.VARCHAR(), + comment="TAC grade (A/B/C/F in the case of HSC queue)", + existing_comment="TAC grade (A/B/C/F and N/A)", + existing_nullable=False, + ) + op.alter_column( + "proposal", + "group_id", + existing_type=sa.VARCHAR(), + comment="Group ID in STARS (e.g., o21195?)", + existing_comment="Group ID in STARS (e.g., o21195)", + existing_nullable=False, + autoincrement=False, + ) + op.alter_column( + "proposal", + "proposal_id", + existing_type=sa.VARCHAR(), + comment="Unique identifier for proposal (e.g, S21B-OT06?)", + existing_comment="Unique identifier for proposal (e.g, S21B-OT06)", + existing_nullable=False, + autoincrement=False, + ) + op.drop_constraint(None, "partner", type_="unique") + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20250505-162126_fc8ea63b8c9d_add_total_fluxes.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20250505-162126_fc8ea63b8c9d_add_total_fluxes.py new file mode 100644 index 0000000..5b35449 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20250505-162126_fc8ea63b8c9d_add_total_fluxes.py @@ -0,0 +1,50 @@ +"""add total fluxes + +Revision ID: fc8ea63b8c9d +Revises: 5b40ed8dcd48 +Create Date: 2025-05-05 16:21:26.652825 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'fc8ea63b8c9d' +down_revision = '5b40ed8dcd48' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('target', sa.Column('total_flux_g', sa.Float(), nullable=True, comment='g-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_r', sa.Float(), nullable=True, comment='r-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_i', sa.Float(), nullable=True, comment='i-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_z', sa.Float(), nullable=True, comment='z-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_y', sa.Float(), nullable=True, comment='y-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_j', sa.Float(), nullable=True, comment='J band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_error_g', sa.Float(), nullable=True, comment='Error in g-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_error_r', sa.Float(), nullable=True, comment='Error in r-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_error_i', sa.Float(), nullable=True, comment='Error in i-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_error_z', sa.Float(), nullable=True, comment='Error in z-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_error_y', sa.Float(), nullable=True, comment='Error in y-band total flux (nJy)')) + op.add_column('target', sa.Column('total_flux_error_j', sa.Float(), nullable=True, comment='Error in J band total flux (nJy)')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('target', 'total_flux_error_j') + op.drop_column('target', 'total_flux_error_y') + op.drop_column('target', 'total_flux_error_z') + op.drop_column('target', 'total_flux_error_i') + op.drop_column('target', 'total_flux_error_r') + op.drop_column('target', 'total_flux_error_g') + op.drop_column('target', 'total_flux_j') + op.drop_column('target', 'total_flux_y') + op.drop_column('target', 'total_flux_z') + op.drop_column('target', 'total_flux_i') + op.drop_column('target', 'total_flux_r') + op.drop_column('target', 'total_flux_g') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20250731-212720_3923581e4392_add_index_target_input_catalog_id.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20250731-212720_3923581e4392_add_index_target_input_catalog_id.py new file mode 100644 index 0000000..2648f90 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20250731-212720_3923581e4392_add_index_target_input_catalog_id.py @@ -0,0 +1,28 @@ +"""add index target.input_catalog_id + +Revision ID: 3923581e4392 +Revises: fc8ea63b8c9d +Create Date: 2025-07-31 21:27:20.574882 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3923581e4392' +down_revision = 'fc8ea63b8c9d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index('target_input_catalog_id_idx', 'target', ['input_catalog_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('target_input_catalog_id_idx', table_name='target') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20250801-202638_89865530fdf1_add_index_in_obj_id_input_catalog_id_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20250801-202638_89865530fdf1_add_index_in_obj_id_input_catalog_id_.py new file mode 100644 index 0000000..271a2d4 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20250801-202638_89865530fdf1_add_index_in_obj_id_input_catalog_id_.py @@ -0,0 +1,32 @@ +"""add index in obj_id, input_catalog_id, and proposal_id for target table + +Revision ID: 89865530fdf1 +Revises: 3923581e4392 +Create Date: 2025-08-01 20:26:38.686170 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '89865530fdf1' +down_revision = '3923581e4392' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index('target_obj_id_input_catalog_id_idx', 'target', ['obj_id', 'input_catalog_id'], unique=False) + op.create_index('target_proposal_id_idx', 'target', ['proposal_id'], unique=False) + op.create_index('target_proposal_id_obj_id_idx', 'target', ['proposal_id', 'obj_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('target_proposal_id_obj_id_idx', table_name='target') + op.drop_index('target_proposal_id_idx', table_name='target') + op.drop_index('target_obj_id_input_catalog_id_idx', table_name='target') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20251107-150645_ce8ac566901b_remove_redundant_unique_constraint_from_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20251107-150645_ce8ac566901b_remove_redundant_unique_constraint_from_.py new file mode 100644 index 0000000..9635958 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20251107-150645_ce8ac566901b_remove_redundant_unique_constraint_from_.py @@ -0,0 +1,142 @@ +"""remove redundant unique constraint from primary keys + +Revision ID: ce8ac566901b +Revises: 89865530fdf1 +Create Date: 2025-11-07 15:06:45.280873 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ce8ac566901b" +down_revision = "89865530fdf1" +branch_labels = None +depends_on = None + + +def upgrade(): + # Remove redundant unique constraints on primary key columns + # These constraints are unnecessary because primary keys already guarantee uniqueness + # and have their own indexes. + # + # Note: Some of these unique constraints may be referenced by foreign keys. + # PostgreSQL foreign keys can reference either unique constraints or primary keys. + # When we drop a unique constraint that's referenced by a foreign key with CASCADE, + # the dependent foreign keys are automatically dropped. + # IMPORTANT: CASCADE does NOT automatically recreate these foreign keys. + # They must be manually recreated (see migration ae27ee4ad568). + + # Drop constraints that are NOT referenced by foreign keys + op.drop_constraint(op.f("sky_sky_id_key"), "sky", type_="unique") + op.drop_constraint(op.f("fluxstd_fluxstd_id_key"), "fluxstd", type_="unique") + op.drop_constraint(op.f("target_target_id_key"), "target", type_="unique") + + # These constraints ARE referenced by foreign keys, so we use raw SQL with CASCADE + # CASCADE will drop the dependent foreign keys (they are recreated in migration ae27ee4ad568) + op.execute("ALTER TABLE pfs_arm DROP CONSTRAINT IF EXISTS pfs_arm_name_key CASCADE") + op.execute( + "ALTER TABLE proposal DROP CONSTRAINT IF EXISTS proposal_proposal_id_key CASCADE" + ) + op.execute( + "ALTER TABLE proposal_category DROP CONSTRAINT IF EXISTS proposal_category_proposal_category_id_key CASCADE" + ) + op.execute( + "ALTER TABLE input_catalog DROP CONSTRAINT IF EXISTS input_catalog_input_catalog_id_key CASCADE" + ) + op.execute( + "ALTER TABLE partner DROP CONSTRAINT IF EXISTS partner_partner_id_key CASCADE" + ) + op.execute( + "ALTER TABLE target_type DROP CONSTRAINT IF EXISTS target_type_target_type_id_key CASCADE" + ) + op.execute( + "ALTER TABLE filter_name DROP CONSTRAINT IF EXISTS filter_name_filter_name_key CASCADE" + ) + + # Recreate one of the foreign keys that was dropped by CASCADE + # Note: This is incomplete - 17 foreign keys were dropped but only 1 is recreated here. + # The remaining 16 foreign keys are recreated in migration ae27ee4ad568. + op.create_foreign_key( + "target_qa_reference_arm_fkey", + "target", + "pfs_arm", + ["qa_reference_arm"], + ["name"], + ) + + +def downgrade(): + # Recreate the redundant unique constraints + # First drop the foreign key that we recreated in upgrade() + op.drop_constraint("target_qa_reference_arm_fkey", "target", type_="foreignkey") + + # Recreate unique constraints + op.create_unique_constraint( + op.f("filter_name_filter_name_key"), + "filter_name", + ["filter_name"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("target_type_target_type_id_key"), + "target_type", + ["target_type_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("partner_partner_id_key"), + "partner", + ["partner_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("input_catalog_input_catalog_id_key"), + "input_catalog", + ["input_catalog_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("proposal_category_proposal_category_id_key"), + "proposal_category", + ["proposal_category_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("pfs_arm_name_key"), + "pfs_arm", + ["name"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("proposal_proposal_id_key"), + "proposal", + ["proposal_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("target_target_id_key"), + "target", + ["target_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("fluxstd_fluxstd_id_key"), + "fluxstd", + ["fluxstd_id"], + postgresql_nulls_not_distinct=False, + ) + op.create_unique_constraint( + op.f("sky_sky_id_key"), "sky", ["sky_id"], postgresql_nulls_not_distinct=False + ) + + # Recreate the foreign key that will now reference the unique constraint + op.create_foreign_key( + "target_qa_reference_arm_fkey", + "target", + "pfs_arm", + ["qa_reference_arm"], + ["name"], + ) diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20251107-191607_ae27ee4ad568_generate_missing_foreign_keys.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20251107-191607_ae27ee4ad568_generate_missing_foreign_keys.py new file mode 100644 index 0000000..4af6ce8 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20251107-191607_ae27ee4ad568_generate_missing_foreign_keys.py @@ -0,0 +1,130 @@ +"""generate missing foreign keys + +Revision ID: ae27ee4ad568 +Revises: ce8ac566901b +Create Date: 2025-11-07 19:16:07.094831 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ae27ee4ad568" +down_revision = "ce8ac566901b" +branch_labels = None +depends_on = None + + +def upgrade(): + # Recreate foreign keys that were dropped by CASCADE in migration ce8ac566901b + # These foreign keys reference primary keys instead of the redundant unique constraints + + # Foreign keys to input_catalog.input_catalog_id + op.create_foreign_key( + "cluster_input_catalog_id_fkey", + "cluster", + "input_catalog", + ["input_catalog_id"], + ["input_catalog_id"], + ) + op.create_foreign_key( + "fluxstd_input_catalog_id_fkey", + "fluxstd", + "input_catalog", + ["input_catalog_id"], + ["input_catalog_id"], + ) + op.create_foreign_key( + "sky_input_catalog_id_fkey", + "sky", + "input_catalog", + ["input_catalog_id"], + ["input_catalog_id"], + ) + op.create_foreign_key( + "target_input_catalog_id_fkey", + "target", + "input_catalog", + ["input_catalog_id"], + ["input_catalog_id"], + ) + op.create_foreign_key( + "user_pointing_input_catalog_id_fkey", + "user_pointing", + "input_catalog", + ["input_catalog_id"], + ["input_catalog_id"], + ) + + # Foreign keys from fluxstd to filter_name.filter_name + op.create_foreign_key( + "fluxstd_filter_g_fkey", "fluxstd", "filter_name", ["filter_g"], ["filter_name"] + ) + op.create_foreign_key( + "fluxstd_filter_r_fkey", "fluxstd", "filter_name", ["filter_r"], ["filter_name"] + ) + op.create_foreign_key( + "fluxstd_filter_i_fkey", "fluxstd", "filter_name", ["filter_i"], ["filter_name"] + ) + op.create_foreign_key( + "fluxstd_filter_z_fkey", "fluxstd", "filter_name", ["filter_z"], ["filter_name"] + ) + op.create_foreign_key( + "fluxstd_filter_y_fkey", "fluxstd", "filter_name", ["filter_y"], ["filter_name"] + ) + op.create_foreign_key( + "fluxstd_filter_j_fkey", "fluxstd", "filter_name", ["filter_j"], ["filter_name"] + ) + + # Foreign keys from target to filter_name.filter_name + op.create_foreign_key( + "target_filter_g_fkey", "target", "filter_name", ["filter_g"], ["filter_name"] + ) + op.create_foreign_key( + "target_filter_r_fkey", "target", "filter_name", ["filter_r"], ["filter_name"] + ) + op.create_foreign_key( + "target_filter_i_fkey", "target", "filter_name", ["filter_i"], ["filter_name"] + ) + op.create_foreign_key( + "target_filter_z_fkey", "target", "filter_name", ["filter_z"], ["filter_name"] + ) + op.create_foreign_key( + "target_filter_y_fkey", "target", "filter_name", ["filter_y"], ["filter_name"] + ) + op.create_foreign_key( + "target_filter_j_fkey", "target", "filter_name", ["filter_j"], ["filter_name"] + ) + + +def downgrade(): + # Drop the foreign keys created in upgrade() + # Note: This downgrade would recreate the situation where foreign keys are missing + # This should only be used if you need to rollback to the state after ce8ac566901b + + # Drop foreign keys from target to filter_name + op.drop_constraint("target_filter_j_fkey", "target", type_="foreignkey") + op.drop_constraint("target_filter_y_fkey", "target", type_="foreignkey") + op.drop_constraint("target_filter_z_fkey", "target", type_="foreignkey") + op.drop_constraint("target_filter_i_fkey", "target", type_="foreignkey") + op.drop_constraint("target_filter_r_fkey", "target", type_="foreignkey") + op.drop_constraint("target_filter_g_fkey", "target", type_="foreignkey") + + # Drop foreign keys from fluxstd to filter_name + op.drop_constraint("fluxstd_filter_j_fkey", "fluxstd", type_="foreignkey") + op.drop_constraint("fluxstd_filter_y_fkey", "fluxstd", type_="foreignkey") + op.drop_constraint("fluxstd_filter_z_fkey", "fluxstd", type_="foreignkey") + op.drop_constraint("fluxstd_filter_i_fkey", "fluxstd", type_="foreignkey") + op.drop_constraint("fluxstd_filter_r_fkey", "fluxstd", type_="foreignkey") + op.drop_constraint("fluxstd_filter_g_fkey", "fluxstd", type_="foreignkey") + + # Drop foreign keys to input_catalog + op.drop_constraint( + "user_pointing_input_catalog_id_fkey", "user_pointing", type_="foreignkey" + ) + op.drop_constraint("target_input_catalog_id_fkey", "target", type_="foreignkey") + op.drop_constraint("sky_input_catalog_id_fkey", "sky", type_="foreignkey") + op.drop_constraint("fluxstd_input_catalog_id_fkey", "fluxstd", type_="foreignkey") + op.drop_constraint("cluster_input_catalog_id_fkey", "cluster", type_="foreignkey") diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20251216-182855_385b6cdf44f7_add_indexes_to_fluxstd.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20251216-182855_385b6cdf44f7_add_indexes_to_fluxstd.py new file mode 100644 index 0000000..5e490f7 --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20251216-182855_385b6cdf44f7_add_indexes_to_fluxstd.py @@ -0,0 +1,30 @@ +"""add indexes to fluxstd + +Revision ID: 385b6cdf44f7 +Revises: ae27ee4ad568 +Create Date: 2025-12-16 18:28:55.673441 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '385b6cdf44f7' +down_revision = 'ae27ee4ad568' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index('ix_fluxstd_input_catalog_fluxstdid', 'fluxstd', ['input_catalog_id', 'fluxstd_id'], unique=False) + op.create_index('ix_fluxstd_version_fluxstdid', 'fluxstd', ['version', 'fluxstd_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_fluxstd_version_fluxstdid', table_name='fluxstd') + op.drop_index('ix_fluxstd_input_catalog_fluxstdid', table_name='fluxstd') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb-dev/alembic/versions/20260129-172838_a251bdccb11f_add_gc_neighbor_and_is_dense_region_.py b/alembic/pfsa-db01-gb-dev/alembic/versions/20260129-172838_a251bdccb11f_add_gc_neighbor_and_is_dense_region_.py new file mode 100644 index 0000000..3b97b8f --- /dev/null +++ b/alembic/pfsa-db01-gb-dev/alembic/versions/20260129-172838_a251bdccb11f_add_gc_neighbor_and_is_dense_region_.py @@ -0,0 +1,40 @@ +"""add gc_neighbor and is_dense_region columns + +Revision ID: a251bdccb11f +Revises: 385b6cdf44f7 +Create Date: 2026-01-29 17:28:38.712851 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a251bdccb11f' +down_revision = '385b6cdf44f7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('fluxstd', sa.Column('is_gc_neighbor', sa.Boolean(), nullable=True, comment='Flag for globular cluster neighbor')) + op.add_column('fluxstd', sa.Column('is_dense_region', sa.Boolean(), nullable=True, comment='Flag for dense stellar region')) + op.alter_column('fluxstd', 'obj_id', + existing_type=sa.BIGINT(), + comment='source_id (e.g., Gaia EDR3, DR3, etc.)', + existing_comment='Gaia EDR3 sourceid', + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('fluxstd', 'obj_id', + existing_type=sa.BIGINT(), + comment='Gaia EDR3 sourceid', + existing_comment='source_id (e.g., Gaia EDR3, DR3, etc.)', + existing_nullable=False) + op.drop_column('fluxstd', 'is_dense_region') + op.drop_column('fluxstd', 'is_gc_neighbor') + # ### end Alembic commands ### diff --git a/alembic/pfsa-db01-gb/alembic.ini b/alembic/pfsa-db01-gb/alembic.ini new file mode 100644 index 0000000..0dd9c2e --- /dev/null +++ b/alembic/pfsa-db01-gb/alembic.ini @@ -0,0 +1,77 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = 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 + +# sys.path path, will be prepended to sys.path if present. +prepend_sys_path = . + +# timezone used for the date in the migration file and its filename. +# UTC matches the rest of targetdb and keeps filenames identical whichever +# machine generates them. Resolved via dateutil, which pandas already pulls in. +timezone = UTC + +# max length of characters to apply to the "slug" field +truncate_slug_length = 40 + +# run env.py during the 'revision' command even without --autogenerate +revision_environment = false + +# allow .pyc/.pyo files without a source .py to be detected as revisions +sourceless = false + +# version path separator; "os" uses os.pathsep. +version_path_separator = os + +# the output encoding used when revision files are written from script.py.mako +output_encoding = utf-8 + +# Fallback only: env.py builds the URL from the TOML file named by the +# TARGETDB_CONF environment variable when it is set. Placeholder -- do not +# commit real credentials. +sqlalchemy.url = postgresql://username:password@pfsa-db01-gb:5433/targetdb + + +[post_write_hooks] +# Deliberately empty: alembic/ is excluded from black and ruff in +# pyproject.toml, so generated revisions are left as alembic writes them. + + +# 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/alembic/pfsa-db01-gb/alembic/README.md b/alembic/pfsa-db01-gb/alembic/README.md deleted file mode 100644 index 3fcbccd..0000000 --- a/alembic/pfsa-db01-gb/alembic/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Generic single-database configuration. - -## Basic workflow - -Reference: https://alembic.sqlalchemy.org/en/latest/tutorial.html - -### Modify code of targetdb - - -### Run alembic - -Create a new revision file. - -```bash -alembic -c revision --autogenerate -m "Add columns" -``` - -Check the output file under the `versions` directory. - -Upgrade to the new revision. - -```bash -alembic -c upgrade head -``` - -### count duplicated rows - -```sql -SELECT COUNT(*) -FROM sky a -JOIN sky b ON a.obj_id = b.obj_id - AND a.input_catalog_id = b.input_catalog_id - AND a.version = b.version - AND a.sky_id < b.sky_id -; -``` - -```sql -SELECT ct, count(*) AS ct_ct FROM (SELECT sky_id, input_catalog_id, version, count(*) AS ct FROM sky GROUP BY sky_id, input_catalog_id, version HAVING count(*) > 1) sub GROUP BY 1 ORDER BY 1; - -SELECT ct, count(*) AS ct_ct FROM (SELECT obj_id, input_catalog_id, version, count(*) AS ct FROM sky WHERE version='20221031' GROUP BY obj_id, input_catalog_id, version HAVING count(*) > 1) sub GROUP BY 1 ORDER BY 1; - -SELECT sky_id, input_catalog_id, version, count(*) FROM sky GROUP BY sky_id, input_catalog_id, version HAVING count(*) > 1; -SELECT DISTINCT version FROM sky; - -SELECT obj_id, ra, dec, input_catalog_id, version, count(*) FROM sky GROUP BY obj_id, input_catalog_id, version HAVING count(*) > 1; - -SELECT version, count(*) AS ct FROM sky GROUP BY 1; - version | ct -----------+------------ - 20220427 | 2747 - 20220915 | 2004836366 - 20221031 | 897209700 -(3 rows) -``` - -### delete duplicated rows - -```sql - -``` - - -### error - -``` -> time alembic -c /work/monodera/Subaru-PFS/alembic_configs/alembic_pfsa-db01-gb.ini upgrade head -INFO [alembic.runtime.migration] Context impl PostgresqlImpl. -INFO [alembic.runtime.migration] Will assume transactional DDL. -INFO [alembic.runtime.migration] Running upgrade 6aec47e0f339 -> eeca77238d00, Add an index and uniqueconstraint on sky - by obj_id, input_catalog_id, and version -Traceback (most recent call last): - File "/work/monodera/pyvenvs/venv39/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1964, in _exec_single -_context - self.dialect.do_execute( - File "/work/monodera/pyvenvs/venv39/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 748, in do_execute - cursor.execute(statement, parameters) -psycopg2.errors.UniqueViolation: could not create unique index "uq_sky_obj_id_input_catalog_id_version" -DETAIL: Key (obj_id, input_catalog_id, version)=(44332, 1002, 20220915) is duplicated. -``` diff --git a/alembic/pfsa-db01-gb/alembic/env.py b/alembic/pfsa-db01-gb/alembic/env.py index fa0e27c..433e1b7 100644 --- a/alembic/pfsa-db01-gb/alembic/env.py +++ b/alembic/pfsa-db01-gb/alembic/env.py @@ -1,9 +1,10 @@ from logging.config import fileConfig -from sqlalchemy import engine_from_config, pool -from targetdb import models +from sqlalchemy import create_engine, pool from alembic import context +from targetdb import models +from targetdb.utils import get_alembic_url # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -26,6 +27,23 @@ # ... etc. +def get_url(): + """Resolve the database URL, preferring the TARGETDB_CONF TOML file. + + Point TARGETDB_CONF at a targetdb config file to keep the credentials in + one place (and out of alembic.ini): + + TARGETDB_CONF=~/database_configs/config_targetdb.toml alembic upgrade head + + Falling back to alembic.ini's sqlalchemy.url when it is unset. + + Note that config.set_main_option() is deliberately not used: ConfigParser + treats "%" as an interpolation marker, so a password containing "%" would + be mangled. Building the engine directly avoids the problem entirely. + """ + return get_alembic_url() or config.get_main_option("sqlalchemy.url") + + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -38,7 +56,7 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") + url = get_url() context.configure( url=url, target_metadata=target_metadata, @@ -58,11 +76,7 @@ def run_migrations_online(): and associate a connection with the context. """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) + connectable = create_engine(get_url(), poolclass=pool.NullPool) with connectable.connect() as connection: context.configure( diff --git a/diagrams/erdiagram_targetdb_latest.pdf b/diagrams/erdiagram_targetdb_latest.pdf index 271f109..6267b55 100644 Binary files a/diagrams/erdiagram_targetdb_latest.pdf and b/diagrams/erdiagram_targetdb_latest.pdf differ diff --git a/docs/getting_started.md b/docs/getting_started.md index 6e408c8..9d4aed5 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -21,22 +21,27 @@ The dependencies are automatically installed when you install the `targetdb` pac Package versions shown here are those used for the development (as of May 2026). Newer (and somewhat older) versions should also work. -| Package | Version | -| ---------------------------------------------------------------------- | ------: | -| [Python](https://www.python.org/) | 3.12.12 | -| [alembic](https://alembic.sqlalchemy.org/en/latest/) | 1.18.0 | -| [Astropy](https://www.astropy.org/) | 7.20 | -| [loguru](https://loguru.readthedocs.io/) | 0.7.3 | -| [NumPy](https://numpy.org) | 2.4.0 | -| [openpyxl](https://openpyxl.readthedocs.io/en/stable/) | 3.1.5 | -| [pandas](https://pandas.pydata.org/) | 2.3.3 | -| [psycopg2-binary](https://www.psycopg.org/) | 2.9.11 | -| [pyarrow](https://arrow.apache.org/docs/python/) | 22.0.0 | -| [requests](https://requests.readthedocs.io/en/latest/) | 2.32.5 | -| [SQLAlchemy](https://www.sqlalchemy.org/) | 2.0.45 | -| [SQLAlchemy-Utils](https://sqlalchemy-utils.readthedocs.io/en/latest/) | 0.42.1 | -| [tabulate](https://pypi.org/project/tabulate/) | 0.9.0 | -| [Typer](https://typer.tiangolo.com/) | 0.21.1 | +| Package | Version | +| ---------------------------------------------------------------------- | ----------: | +| [Python](https://www.python.org/) | 3.12.12 | +| [alembic](https://alembic.sqlalchemy.org/en/latest/) | 1.18.0 | +| [Astropy](https://www.astropy.org/) | 7.20 | +| [loguru](https://loguru.readthedocs.io/) | 0.7.3 | +| [NumPy](https://numpy.org) | 2.4.0 | +| [openpyxl](https://openpyxl.readthedocs.io/en/stable/) | 3.1.5 | +| [pandas](https://pandas.pydata.org/) | 2.3.3 | +| [pfs-utils](https://github.com/Subaru-PFS/pfs_utils) | 7.2026.3100 | +| [psycopg](https://www.psycopg.org/) | 3.3.4 | +| [pyarrow](https://arrow.apache.org/docs/python/) | 22.0.0 | +| [requests](https://requests.readthedocs.io/en/latest/) | 2.32.5 | +| [SQLAlchemy](https://www.sqlalchemy.org/) | 2.0.45 | +| [SQLAlchemy-Utils](https://sqlalchemy-utils.readthedocs.io/en/latest/) | 0.42.1 | +| [tabulate](https://pypi.org/project/tabulate/) | 0.9.0 | +| [Typer](https://typer.tiangolo.com/) | 0.21.1 | + +`pfs-utils` is installed straight from GitHub (`targetdb.TargetDB` subclasses +`pfs.utils.database.db.DB`), so a `.git` directory and network access are needed +at install time. For building the documentation, the following packages are required. @@ -85,7 +90,7 @@ host = "localhost" # database host port = 5432 # database port dbname = "targetdb" # database name user = "admin" # database user -password = "admin" # database password +password = "admin" # database password (optional, see below) dialect = "postgresql" # database dialect @@ -93,6 +98,42 @@ dialect = "postgresql" # database dialect SCHEMACRAWLERDIR = "" # "_schemacrawler/bin/schemacrawler.sh" under the path will be used ``` +`dialect = "postgresql"` is resolved to the psycopg3 driver +(`postgresql+psycopg`); psycopg2 is no longer used. Existing configuration +files need no change. + +### Keeping the password out of `dbconf.toml` + +`password` is optional. Leave it out and it is omitted from the connection URL +entirely, so libpq resolves the credential itself -- first `PGPASSWORD`, then +`PGPASSFILE` or `~/.pgpass`. This lets a configuration file with no secrets in +it be kept under version control. + +```toml title="dbconf.toml (no password)" +[targetdb.db] +host = "pfsa-db.example.org" +port = 5433 +dbname = "targetdb" +user = "admin" +dialect = "postgresql" +``` + +```text title="~/.pgpass" +pfsa-db.example.org:5433:targetdb:admin:the-actual-password +``` + +Two things to watch out for: + +- Each `~/.pgpass` field is matched against the corresponding TOML value **as a + literal string**. `localhost` and a fully qualified domain name are different + hosts as far as libpq is concerned, even when they resolve to the same server. + `*` may be used as a wildcard for any of the first four fields. +- The file must be mode `0600`. libpq ignores it silently otherwise -- there is + no warning, only an authentication failure. + +When `password` _is_ present in the TOML it is used as before and takes +precedence, which is the same ordering libpq itself applies. + The following commands are to create the `targetdb` database, install the Q3C extension, create tables, and generate an entity-relationship diagram of the database. @@ -129,7 +170,7 @@ To connect to the database with Python, you can use the following code snippet. from targetdb import TargetDB # create a database connection -db = TargetDB(host="localhost", port= 5432, dbname="testdb", user="admin", password="admin") +db = TargetDB(host="localhost", port=5432, dbname="testdb", user="admin", password="admin") db.connect() # fetch all data from the input_catalog table as pandas.DataFrame @@ -145,6 +186,66 @@ print(df.head()) db.close() ``` +### Connecting without arguments + +`TargetDB()` can be called with no arguments at all. Any parameter left +unset falls back to a class default (`TargetDB.DEFAULT_HOST`, +`DEFAULT_PORT`, `DEFAULT_DBNAME`, `DEFAULT_USER`), which point at the +production database using `obsproc`, a **read-only** account: + +```python +from targetdb import TargetDB + +with TargetDB() as db: + df = db.fetch_all("input_catalog") +``` + +Because `obsproc` has no password default, this only works once +`~/.pgpass` has a matching entry (mode `0600`, as described above): + +```text title="~/.pgpass" +pfsa-db:5433:targetdb:obsproc: +``` + +`obsproc` can only `SELECT`; inserting or updating still requires a config +file with an explicit, privileged `user` (as in the `pfs-targetdb-cli` +examples above). To point the bare `TargetDB()` form at a different +database for a whole process -- a dev instance, say -- call +`TargetDB.set_default_connection(...)` once before constructing any +instances: + +```python +TargetDB.set_default_connection(host="pfsa-db", port=5437, dbname="targetdb_dev") +db = TargetDB() # now defaults to targetdb_dev +``` + +## Database Migrations with Alembic + +Each deployment target has its own directory under `alembic/` +(`local_test/`, `pfsa-db01-gb/`, `pfsa-db01-gb-dev/`), each with its own +`alembic.ini` and revision history. Run alembic from within the relevant +directory. + +The `env.py` scripts build the connection URL from the same TOML configuration +file the CLI uses. Point `TARGETDB_CONF` at it: + +```bash +cd alembic/local_test +TARGETDB_CONF=~/database_configs/config_targetdb.toml alembic upgrade head +``` + +This keeps the credentials in one place instead of duplicating them between +`dbconf.toml` and `alembic.ini`, and means the switch to psycopg3 is picked up +automatically without hand-editing the `sqlalchemy.url` of each deployment. +Combined with the `~/.pgpass` arrangement above, an `alembic.ini` with no +secrets in it can be kept in the repository. + +When `TARGETDB_CONF` is not set, `env.py` falls back to the `sqlalchemy.url` +entry in `alembic.ini` as before. + +See `alembic/README.md` for the full migration workflow and the gotchas specific +to this schema. + ## Running Tests Locally The unit tests (`tests/test_*.py`, excluding `tests/integration/`) do not require a database diff --git a/docs/tbls/public.fluxstd.md b/docs/tbls/public.fluxstd.md index 8a0e7d2..e02e860 100644 --- a/docs/tbls/public.fluxstd.md +++ b/docs/tbls/public.fluxstd.md @@ -101,10 +101,10 @@ | ---- | ---------- | | fluxstd_pkey | CREATE UNIQUE INDEX fluxstd_pkey ON public.fluxstd USING btree (fluxstd_id) | | uq_obj_id_input_catalog_id_version | CREATE UNIQUE INDEX uq_obj_id_input_catalog_id_version ON public.fluxstd USING btree (obj_id, input_catalog_id, version) | -| ix_fluxstd_version_fluxstdid | CREATE INDEX ix_fluxstd_version_fluxstdid ON public.fluxstd USING btree (version, fluxstd_id) | | ix_fluxstd_version | CREATE INDEX ix_fluxstd_version ON public.fluxstd USING btree (version) | -| fluxstd_q3c_ang2ipix_idx | CREATE INDEX fluxstd_q3c_ang2ipix_idx ON public.fluxstd USING btree (q3c_ang2ipix(ra, "dec")) | +| ix_fluxstd_version_fluxstdid | CREATE INDEX ix_fluxstd_version_fluxstdid ON public.fluxstd USING btree (version, fluxstd_id) | | ix_fluxstd_input_catalog_fluxstdid | CREATE INDEX ix_fluxstd_input_catalog_fluxstdid ON public.fluxstd USING btree (input_catalog_id, fluxstd_id) | +| fluxstd_q3c_ang2ipix_idx | CREATE INDEX fluxstd_q3c_ang2ipix_idx ON public.fluxstd USING btree (q3c_ang2ipix(ra, "dec")) | ## Relations diff --git a/docs/tbls/public.sky.md b/docs/tbls/public.sky.md index 1acf434..8df7986 100644 --- a/docs/tbls/public.sky.md +++ b/docs/tbls/public.sky.md @@ -34,9 +34,9 @@ | ---- | ---------- | | sky_pkey | CREATE UNIQUE INDEX sky_pkey ON public.sky USING btree (sky_id) | | sky_obj_id_input_catalog_id_version_key | CREATE UNIQUE INDEX sky_obj_id_input_catalog_id_version_key ON public.sky USING btree (obj_id, input_catalog_id, version) | -| ix_sky_input_catalog_id | CREATE INDEX ix_sky_input_catalog_id ON public.sky USING btree (input_catalog_id) | | ix_sky_version | CREATE INDEX ix_sky_version ON public.sky USING btree (version) | | sky_q3c_ang2ipix_idx | CREATE INDEX sky_q3c_ang2ipix_idx ON public.sky USING btree (q3c_ang2ipix(ra, "dec")) | +| ix_sky_input_catalog_id | CREATE INDEX ix_sky_input_catalog_id ON public.sky USING btree (input_catalog_id) | ## Relations diff --git a/examples/data/proposals.csv b/examples/data/proposals.csv index 3ecdab6..655ca93 100644 --- a/examples/data/proposals.csv +++ b/examples/data/proposals.csv @@ -14,14 +14,14 @@ S23A-QN900,o22016,Kiyoto,Yabe0,None,6.0,c,36000.0,0.0,0.0,1,subaru,False S23A-QN901,o22016,Kiyoto,Yabe1,None,7.0,b,12000.0,0.0,0.0,1,subaru,False S23A-QN902,o22016,Kiyoto,Yabe2,None,8.0,b,7200.0,0.0,0.0,1,subaru,False S23A-QN903,o22016,Kiyoto,Yabe3,None,9.0,a,3600.0,0.0,0.0,1,subaru,False -S23B-QT901,,M.,Ishigaki,,10.0,A,595.5,319.5,276.0,1,subaru,False -S23B-QT902,,K.,Yabe,,8.8,A,1730.0,1730.0,0.0,1,subaru,False -S23B-QT903,,W.,He,,9.1,B,234.5,0.0,234.5,1,subaru,False -S23B-QT904,,M.,Tanaka,,8.2,B,62.25,0.0,62.25,1,subaru,False -S23B-QT905,,M.,Onodera,,7.5,B,5166.0,0.0,5166.0,1,subaru,False -S23B-QT906,,Y.,Moritani,,7.0,A,2542.25,2542.25,0.0,1,subaru,False -S23B-QT907,,J.,Eric,,6.5,C,2594.5,2594.5,0.0,1,subaru,False -S23B-QT908,,Y.,Takagi,,5.6,C,320.0,320.0,0.0,1,subaru,False +S23B-QT901,o23901,M.,Ishigaki,,10.0,A,595.5,319.5,276.0,1,subaru,False +S23B-QT902,o23902,K.,Yabe,,8.8,A,1730.0,1730.0,0.0,1,subaru,False +S23B-QT903,o23903,W.,He,,9.1,B,234.5,0.0,234.5,1,subaru,False +S23B-QT904,o23904,M.,Tanaka,,8.2,B,62.25,0.0,62.25,1,subaru,False +S23B-QT905,o23905,M.,Onodera,,7.5,B,5166.0,0.0,5166.0,1,subaru,False +S23B-QT906,o23906,Y.,Moritani,,7.0,A,2542.25,2542.25,0.0,1,subaru,False +S23B-QT907,o23907,J.,Eric,,6.5,C,2594.5,2594.5,0.0,1,subaru,False +S23B-QT908,o23908,Y.,Takagi,,5.6,C,320.0,320.0,0.0,1,subaru,False S24B-QT901,o00001,Sakurako,Okamoto,NaN,9.0,A,284.25,0.0,284.25,1,subaru,False S24B-QT902,o00002,Masayuki,Tanaka,NaN,8.5,B,951.0,0.0,951.0,1,subaru,False S24B-QT903,o00003,Tae-Soo,Pyo,NaN,8.5,B,3190.75,0.0,3190.75,1,subaru,False diff --git a/pyproject.toml b/pyproject.toml index 96d24ea..cacb8b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ets_target_database" description = "PFS target database (targetDB) tools" readme = "README.md" -requires-python = ">=3.11, <3.13" +requires-python = ">=3.12, <3.13" license = "MIT" license-files = ["LICENSE"] authors = [{ name = "Masato Onodera", email = "monodera@naoj.org" }] @@ -11,7 +11,6 @@ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Database", "Topic :: Scientific/Engineering :: Astronomy", @@ -23,7 +22,12 @@ dependencies = [ "numpy>=2.0", "openpyxl", "pandas", - "psycopg2-binary", + # TargetDB subclasses pfs.utils.database.db.DB; tracking master because its + # database/ module is still actively developed. `uv.lock` pins the resolved + # commit, so picking up upstream changes needs an explicit + # `uv lock --upgrade-package pfs-utils` followed by the integration tests. + "pfs-utils @ git+https://github.com/Subaru-PFS/pfs_utils.git@master", + "psycopg[binary]", "pyarrow", "requests", "sqlalchemy", @@ -31,7 +35,6 @@ dependencies = [ "sqlalchemy-utils", "tabulate", # "wheel", - 'tomli >= 1.1.0 ; python_version < "3.11"', "typer", ] # version = "0.1.0" @@ -70,40 +73,31 @@ markers = [ line-length = 88 # Generated by setuptools-scm and gitignored; see the matching ruff # extend-exclude below for why it's kept out of linting/formatting. -# alembic/ migrations are historical records and are deliberately left +# alembic revision scripts are historical records and are deliberately left # unformatted -- excluded here too so an editor's black extension with -# format-on-save doesn't rewrite them. -extend-exclude = "(src/targetdb/_version\\.py|alembic/)" +# format-on-save doesn't rewrite them. Only versions/ is excluded: env.py is +# live code and is formatted like the rest of the package. +extend-exclude = "(src/targetdb/_version\\.py|alembic/.*/versions/)" [tool.pdm.scripts] serve-doc = { shell = "mkdocs serve", help = "Start the dev server for doc preview" } build-doc = { shell = "mkdocs build", help = "Build documentation" } -gen-requirements = { cmd = [ - "pdm", - "export", - "--format", - "requirements", - "--without-hashes", - "--pyproject", - "--dev", - "--output", - "requirements.txt", - "--verbose", -], help = "Generate requirements.txt" } [tool.ruff] -# Matches requires-python (">=3.11"). Do not raise this without raising +# Matches requires-python (">=3.12"). Do not raise this without raising # requires-python: with UP rules enabled, ruff rewrites code to the target # version's syntax. -target-version = "py311" +target-version = "py312" line-length = 88 # Generated by setuptools-scm and gitignored. It exists locally but not in a # fresh CI checkout, so linting it makes the checked file set environment- # dependent. -# alembic/ migrations are historical records and are deliberately not -# linted -- excluding it here (not just in the runners' arguments) keeps -# editors and a bare `ruff check .` in agreement with CI. -extend-exclude = ["src/targetdb/_version.py", "alembic"] +# alembic revision scripts are historical records and are deliberately not +# linted -- excluding them here (not just in the runners' arguments) keeps +# editors and a bare `ruff check .` in agreement with CI. Only versions/ is +# excluded; the env.py scripts are live code and are linted like the rest of +# the package. +extend-exclude = ["src/targetdb/_version.py", "alembic/*/alembic/versions"] [tool.ruff.lint] # C4 is flake8-comprehensions, unrelated to C901 (mccabe), which stays off. diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d03e856..0000000 --- a/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is @generated by PDM. -# Please do not edit it manually. - -alembic -astropy -loguru -numpy>=2.0 -openpyxl -pandas -psycopg2-binary -pyarrow -sqlalchemy -sqlalchemy-utils -tabulate -tomli>=1.1.0; python_version < "3.11" -typer diff --git a/src/targetdb/targetdb.py b/src/targetdb/targetdb.py index 7661431..ef2f43e 100644 --- a/src/targetdb/targetdb.py +++ b/src/targetdb/targetdb.py @@ -1,56 +1,146 @@ #!/usr/bin/env python -import io +from contextlib import contextmanager import pandas as pd from loguru import logger -from sqlalchemy import create_engine, select -from sqlalchemy.orm import sessionmaker -from sqlalchemy.sql import text +from pfs.utils.database.db import DB +from sqlalchemy import URL, and_, bindparam, select from . import models -class TargetDB: - # url = "postgresql://pfs@db-ics:5432/opdb" +class TargetDB(DB): + """PFS targetDB accessor built on top of ``pfs.utils.database.db.DB``. + + ``DB`` supplies the engine cache, ``pool_pre_ping``, ``COPY``-based bulk + inserts and the ``query_*`` family. Only what ``DB`` does not provide is + implemented here: + + * a password and an explicit dialect in the URL (``DB`` always omits the + password and hardcodes ``postgresql+psycopg``), + * ``update()``, + * the ``dry_run=`` keyword that backs the CLI's ``--commit`` default, + * ``close()``. + + The public methods kept for backwards compatibility (``connect``, + ``close``, ``fetch_query``, ``fetch_all``, ``fetch_by_id``) are all used by + downstream packages (``ets_pointing``, ``pfs_obsproc_planning_tools``). + + Connection parameters default to ``DEFAULT_HOST``/``DEFAULT_USER``/ + ``DEFAULT_DBNAME``/``DEFAULT_PORT`` below, the same convention + ``pfs.utils.database.db`` uses for ``OpDB`` and ``QaDB``: ``DB.__init__`` + resolves any argument left as ``None`` from ``type(self).DEFAULT_*``, so + declaring these class attributes is also what makes the inherited + ``set_default_connection()`` classmethod usable on ``TargetDB``. The + default user, ``obsproc``, is read-only in production -- every write path + in this package (the CLI, ``utils.add_database_rows``, etc.) passes an + explicit ``user`` from a config file instead of relying on these + defaults, so a bare ``TargetDB()`` can read but never write. + """ + + DEFAULT_HOST = "pfsa-db" + DEFAULT_USER = "obsproc" + DEFAULT_DBNAME = "targetdb" + DEFAULT_PORT = 5433 + # Not part of DB: DB.url hardcodes postgresql+psycopg, TargetDB.url does + # not, so TargetDB needs its own default for the dialect argument below. + DEFAULT_DIALECT = "postgresql" def __init__( self, - host="localhost", - port: int = 5432, - dbname=None, - user=None, - password=None, - dialect="postgresql", + host: str | None = None, + port: int | None = None, + dbname: str | None = None, + user: str | None = None, + password: str | None = None, + dialect: str | None = None, ): - for param in [dbname, user, password]: - if param is None: - logger.error(f"{param} is not provided") - raise ValueError(f"{param} is not provided") + # Set before super().__init__(): the url property below reads both, and + # anything the base class does must already see them. + # `password` is deliberately optional and has no DEFAULT_PASSWORD: + # when it is None it is left out of the URL and libpq resolves it + # (PGPASSWORD, then ~/.pgpass), which is exactly the arrangement DB + # itself documents. + self._password = password + # Imported lazily to avoid a circular import (utils imports TargetDB). + from .utils import normalize_drivername + + self._drivername = normalize_drivername( + dialect if dialect is not None else type(self).DEFAULT_DIALECT + ) + self._dry_run = False + + super().__init__(host=host, user=user, dbname=dbname, port=port) + + # The CLI and every other write path splat a full [targetdb.db] table, + # so this never fires there. It fires exactly when a parameter was + # omitted and a class default therefore decided which database (and, + # for `user`, which privilege level) this instance talks to. + defaulted = [ + name + for name, value in [ + ("host", host), + ("port", port), + ("dbname", dbname), + ("user", user), + ] + if value is None + ] + if defaulted: + logger.info( + f"Using TargetDB class defaults for {', '.join(defaulted)}: " + f"{self.user}@{self.host}:{self.port}/{self.dbname}" + ) - self.dbinfo = f"{dialect}://{user}:{password}@{host}:{port}/{dbname}" + @property + def url(self) -> str: + """Connection URL, including the password when one was configured. - def connect(self): - self.engine = create_engine(self.dbinfo) - SessionClass = sessionmaker(self.engine) - self.session = SessionClass() - # print('connection to {0} started'.format(self.dbinfo)) + Overrides ``DB.url``, which always omits the password and hardcodes the + ``postgresql+psycopg`` driver. - def close(self): - try: - self.session.close() - finally: - # Dispose the engine to release all pooled connections back to the database - # server. Without this, SQLAlchemy's connection pool keeps the underlying - # TCP sockets open until the engine is garbage-collected, which may never - # happen on abnormal process termination. - self.engine.dispose() - # print('connection to {0} closed'.format(self.dbinfo)) + Note + ---- + ``DB.engine`` caches engines in a process-global dict keyed by this + string, so a configured password is held in that key for the lifetime + of the process. Never log this value directly -- use + ``URL.create(...).render_as_string()`` (which masks it) instead. + """ + return URL.create( + drivername=self._drivername, + username=self.user, + password=self._password, + host=self.host, + port=self.port, + database=self.dbname, + ).render_as_string(hide_password=False) + + def connect(self) -> None: + """Materialize the engine. Deliberately returns nothing. + + ``DB.connect()`` returns a pooled ``Connection``; callers here (and in + the downstream packages) discard the return value, which would leak one + checked-out connection per call. ``DB.connection()`` is the only + internal user of ``DB.connect()`` and is overridden below, so narrowing + this to "make sure the engine exists" is safe. + """ + _ = self.engine + + def close(self) -> None: + """Dispose the engine, closing every pooled connection. + + ``DB`` has no ``close()``. Without this, connections linger until the + engine is garbage-collected and ``drop-db`` fails with "database is + being accessed by other users". ``Engine.dispose()`` replaces the pool + rather than invalidating the engine, so the instance cached in + ``pfs.utils.database.db._DB_ENGINES`` stays usable afterwards. + """ + self.engine.dispose() def __enter__(self): """Support usage as a context manager: ``with TargetDB(...) as db:``.""" - if not hasattr(self, "session"): - self.connect() + self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -58,79 +148,62 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False # re-raise any exception - def reset_all(self, full=True): - # - # Order of the resetting tables is important - # - self.session.query(models.cluster).delete() - self.session.query(models.target).delete() - self.session.query(models.fluxstd).delete() - self.session.query(models.sky).delete() - self.session.query(models.proposal).delete() - self.session.query(models.input_catalog).delete() - self.session.query(models.target_type).delete() - self.session.query(models.proposal_category).delete() - self.session.execute("ALTER SEQUENCE target_target_id_seq RESTART WITH 1") - self.session.execute("ALTER SEQUENCE fluxstd_fluxstd_id_seq RESTART WITH 1") - self.session.execute("ALTER SEQUENCE sky_sky_id_seq RESTART WITH 1") - - self.session.commit() - - def reset_target(self): - self.session.query(models.target).delete() - self.session.execute("ALTER SEQUENCE target_target_id_seq RESTART WITH 1") - - def reset_fluxstd(self): - self.session.query(models.fluxstd).delete() - self.session.execute("ALTER SEQUENCE fluxstd_fluxstd_id_seq RESTART WITH 1") - - def reset_sky(self): - self.session.query(models.sky).delete() - self.session.execute("ALTER SEQUENCE sky_sky_id_seq RESTART WITH 1") - - def rollback(self): - self.session.rollback() - - # functionality to insert/update information into the database - - def insert_mappings( - self, tablename, mappings, return_defaults=False, dry_run=False - ): - """ - Description - ----------- - Insert information into a table - Parameters - ---------- - tablename : `string` - mappings : `dictionnary list` - Returns - ------- - None - """ - model = getattr(models, tablename) + @contextmanager + def _dry_run_scope(self, dry_run): + """Temporarily flip the dry-run flag consulted by ``connection()``.""" + previous = self._dry_run + self._dry_run = dry_run try: - # print(mappings) - self.session.bulk_insert_mappings( - model, mappings, return_defaults=return_defaults - ) - if dry_run: - self.session.rollback() - return None - else: - self.session.commit() - - if return_defaults: - df_ret = pd.DataFrame.from_records(mappings) - return df_ret + yield + finally: + self._dry_run = previous + + @contextmanager + def connection(self): + """Pooled connection that commits, or rolls back under dry-run. + + Overrides ``DB.connection()``, which always commits. Every ``DB`` method + that touches the database goes through here, so honouring ``_dry_run`` + at this one point makes ``insert``/``update``/``execute_query`` all obey + the CLI's ``--commit`` contract -- including the ``COPY`` fast path, + whose raw cursor runs inside this same transaction. + + The transaction is opened eagerly rather than left to SQLAlchemy's + implicit begin: ``pandas.DataFrame.to_sql`` starts *and commits* a + transaction of its own when handed a connection that is not already in + one, which would make the rollback below a no-op. + """ + with self.engine.connect() as conn: + conn.begin() + yield conn + if self._dry_run: + conn.rollback() else: - return None - # print(mappings) - except Exception as e: - self.session.rollback() - raise e - - def insert(self, tablename, dataframe, return_defaults=False, dry_run=False): + conn.commit() + + # ################################################## + # functionality to insert/update information + # ################################################## + + @staticmethod + def _drop_unknown_columns(tablename, dataframe): + """Drop DataFrame columns the table does not have. + + `add_backref_values()` resolves foreign keys by merging whole reference + tables into the DataFrame, which leaves descriptive columns behind + (`partner_name` and `proposal_category_name` when inserting proposals, + for instance). The ORM `bulk_insert_mappings()` path this class used to + take ignored keys that were not mapped to a column; `COPY` and Core + `update()` do not, so the same leniency is applied explicitly here. + """ + known = set(getattr(models, tablename).__table__.columns.keys()) + extra = [column for column in dataframe.columns if column not in known] + if not extra: + return dataframe + logger.debug(f"Ignoring columns absent from the {tablename} table: {extra}") + return dataframe.drop(columns=extra) + + def insert(self, tablename, dataframe, dry_run=False): """ Description ----------- @@ -139,47 +212,20 @@ def insert(self, tablename, dataframe, return_defaults=False, dry_run=False): ---------- tablename : `string` dataframe : `pandas.DataFrame` + dry_run : `bool` roll back instead of committing Returns ------- - None - Note - ---- - Column labels of `dataframe` should be exactly the same as those of the table - """ - mappings_dict = dataframe.to_dict(orient="records") - df_ret = self.insert_mappings( - tablename, mappings_dict, return_defaults=return_defaults, dry_run=dry_run - ) - if return_defaults: - return df_ret - else: - return None - - def insert_by_copy(self, tablename, data, colnames, dry_run=False): - """ - Description - ----------- - Insert information into a table using COPY FROM method - Parameters - ---------- - tablename : `string` - data : `a text stream` - colnames: `list` of `string` - Returns - ------- - None + n_inserted : `int` or `None` Note ---- + Column labels of `dataframe` should be exactly the same as those of + the table. Delegates to `DB.insert_dataframe()`, which uses + PostgreSQL's `COPY`. """ - conn = self.engine.raw_connection() - cur = conn.cursor() - cur.copy_from(data, tablename, ",", columns=colnames) - if dry_run: - conn.rollback() - else: - conn.commit() - cur.close() - conn.close() + with self._dry_run_scope(dry_run): + return self.insert_dataframe( + tablename, self._drop_unknown_columns(tablename, dataframe) + ) def update(self, tablename, dataframe, dry_run=False): """ @@ -190,25 +236,60 @@ def update(self, tablename, dataframe, dry_run=False): ---------- tablename : `string` dataframe : `pandas.DataFrame` + dry_run : `bool` roll back instead of committing Returns ------- None Note ---- - Column labels of `dataframe` should be exactly the same as those of the table + Column labels of `dataframe` should be exactly the same as those of + the table, and must include the primary key column(s), which are + matched on rather than written. `DB` has no equivalent, so this is a + single Core UPDATE statement executed once per row by executemany. """ - model = getattr(models, tablename) - try: - self.session.bulk_update_mappings( - model, dataframe.to_dict(orient="records") + table = getattr(models, tablename).__table__ + dataframe = self._drop_unknown_columns(tablename, dataframe) + records = dataframe.to_dict(orient="records") + if not records: + logger.warning(f"No rows to update in the {tablename} table.") + return + + pk_names = [c.name for c in table.primary_key.columns] + missing = [name for name in pk_names if name not in dataframe.columns] + if missing: + raise ValueError( + f"Primary key column(s) {missing} missing from the DataFrame " + f"for the {tablename} table." ) - if dry_run: - self.session.rollback() - else: - self.session.commit() - except: - self.session.rollback() - raise + + value_names = [name for name in dataframe.columns if name not in pk_names] + if not value_names: + raise ValueError( + f"The DataFrame for the {tablename} table contains only primary " + "key columns; there is nothing to update." + ) + + # The primary key values are bound under a "_pk_" prefix so that they do + # not collide with the SET parameters of the same name. + for record in records: + for name in pk_names: + record[f"_pk_{name}"] = record.pop(name) + + stmt = ( + table.update() + .where( + and_( + *[ + c == bindparam(f"_pk_{c.name}") + for c in table.primary_key.columns + ] + ) + ) + .values({name: bindparam(name) for name in value_names}) + ) + + with self._dry_run_scope(dry_run), self.connection() as conn: + conn.execute(stmt, records) """ ################################################## @@ -216,6 +297,11 @@ def update(self, tablename, dataframe, dry_run=False): ################################################## """ + def _fetch(self, stmt): + """Run a Core SELECT and return the result as a DataFrame.""" + with self.connection() as conn: + return pd.read_sql(stmt, con=conn) + def fetch_all(self, tablename): """ Description @@ -229,23 +315,11 @@ def fetch_all(self, tablename): df : `pandas.DataFrame` Note ---- + Uses a Core `select()` on the model's `Table` rather than + `DB.query_dataframe()`, which would require interpolating the table + name into a SQL string. """ - model = getattr(models, tablename) - try: - # Use session.execute() with select() to avoid immutabledict issues with pd.read_sql() - stmt = select(model) - result = self.session.execute(stmt) - # Get column names from the model's mapper - columns = [c.key for c in model.__mapper__.columns] - data = result.fetchall() - # Extract values from Row objects (each row is a tuple with one element - the model instance) - rows = [[getattr(row[0], col) for col in columns] for row in data] - df = pd.DataFrame(rows, columns=columns) - except: - self.session.rollback() - raise - - return df + return self._fetch(select(getattr(models, tablename).__table__)) def fetch_by_id(self, tablename, **kwargs): """ @@ -262,24 +336,11 @@ def fetch_by_id(self, tablename, **kwargs): Note ---- """ - model = getattr(models, tablename) - try: - # Use session.execute() with select() to avoid immutabledict issues with pd.read_sql() - stmt = select(model) - for k, v in kwargs.items(): - stmt = stmt.filter(getattr(model, k) == v) - result = self.session.execute(stmt) - # Get column names from the model's mapper - columns = [c.key for c in model.__mapper__.columns] - data = result.fetchall() - # Extract values from Row objects (each row is a tuple with one element - the model instance) - rows = [[getattr(row[0], col) for col in columns] for row in data] - df = pd.DataFrame(rows, columns=columns) - except: - self.session.rollback() - raise - - return df + table = getattr(models, tablename).__table__ + stmt = select(table) + for k, v in kwargs.items(): + stmt = stmt.where(table.c[k] == v) + return self._fetch(stmt) def fetch_query(self, query): """ @@ -294,42 +355,9 @@ def fetch_query(self, query): df : `pandas.DataFrame` Note ---- + Delegates to `DB.query_dataframe()`. """ - try: - # Use session.execute() with text() to avoid immutabledict issues with pd.read_sql() - result = self.session.execute(text(query)) - columns = result.keys() - data = result.fetchall() - df = pd.DataFrame(data, columns=columns) - except: - self.session.rollback() - raise - - return df - - def fetch_by_copy(self, tablename, colnames): - """ - Description - ----------- - Get selected records from a table by using COPY TO method - Parameters - ---------- - tablename : `string` - colnames : `list` of `string` - Returns - ------- - data : `io.StringIO` (comma-separated) - Note - ---- - """ - data = io.StringIO() - conn = self.engine.raw_connection() - cur = conn.cursor() - cur.copy_to(data, tablename, sep=",", null="\\N", columns=colnames) - cur.close() - conn.close() - data.seek(0) - return data + return self.query_dataframe(query) def execute_query(self, query, dry_run=False): """ @@ -338,19 +366,14 @@ def execute_query(self, query, dry_run=False): Execute a SQL query Parameters ---------- - query : `string` + query : `string` + dry_run : `bool` roll back instead of committing Returns ------- None Note ---- + Delegates to `DB.commit()`. """ - try: - self.session.execute(text(query)) - if dry_run: - self.session.rollback() - else: - self.session.commit() - except Exception as e: - self.session.rollback() - raise e + with self._dry_run_scope(dry_run): + self.commit(query) diff --git a/src/targetdb/utils.py b/src/targetdb/utils.py index e361dd6..9b3b62a 100644 --- a/src/targetdb/utils.py +++ b/src/targetdb/utils.py @@ -7,6 +7,7 @@ import subprocess import tempfile import time +import tomllib import zipfile from datetime import datetime from pathlib import Path @@ -26,11 +27,6 @@ input_catalog_id_start, ) -try: - import tomllib -except ModuleNotFoundError: - import tomli as tomllib - def load_config(config_file): """ @@ -177,6 +173,43 @@ def read_excel(input_file, sheetnames=None): # return {"proposal": df_proposal, "allocation": df_allocation} +# targetdb speaks to PostgreSQL through psycopg3 only; psycopg2 is no longer a +# dependency. Existing dbconf.toml files say `dialect = "postgresql"`, which +# SQLAlchemy resolves to psycopg2 by default, so the bare dialect names are +# mapped to the psycopg3 driver here. Doing it in one place means no deployed +# configuration file has to be edited. +_DRIVER_ALIASES = { + "postgresql": "postgresql+psycopg", + "postgres": "postgresql+psycopg", +} + + +def normalize_drivername(dialect): + """ + Map a bare PostgreSQL dialect name onto the psycopg3 driver. + + Parameters + ---------- + dialect : str + The dialect string from the configuration file (e.g. ``"postgresql"``). + + Returns + ------- + drivername : str + ``"postgresql+psycopg"`` for the bare PostgreSQL dialect names, and the + input unchanged for anything else (an explicit ``dialect+driver`` value + is always honoured as written). + + Examples + -------- + >>> normalize_drivername("postgresql") + 'postgresql+psycopg' + >>> normalize_drivername("postgresql+psycopg2") + 'postgresql+psycopg2' + """ + return _DRIVER_ALIASES.get(dialect, dialect) + + def get_url_object(config): """ Create a URL object from the given configuration. @@ -185,7 +218,8 @@ def get_url_object(config): ---------- config : dict The configuration dictionary containing the database connection details. - Expected keys are 'targetdb'->'db'->'dialect', 'user', 'password', 'host', 'port', 'dbname'. + Expected keys are 'targetdb'->'db'->'dialect', 'user', 'host', 'port', + and 'dbname'. 'password' is optional. Returns ------- @@ -197,17 +231,23 @@ def get_url_object(config): KeyError If a necessary key is missing from the config dictionary. + Notes + ----- + ``password`` is optional. When it is absent the password is left out of the + URL entirely and libpq resolves it itself (``PGPASSWORD``, then + ``PGPASSFILE`` / ``~/.pgpass``). See ``docs/getting_started.md``. + Examples -------- >>> config = {'targetdb': {'db': {'dialect':'postgresql', 'user':'username', 'password':'password', 'host':'localhost', 'port':5432, 'dbname':'test_db'}}} >>> url_object = get_url_object(config) >>> print(url_object) - postgresql://username:password@localhost:5432/test_db + postgresql+psycopg://username:***@localhost:5432/test_db """ url_object = URL.create( - drivername=config["targetdb"]["db"]["dialect"], + drivername=normalize_drivername(config["targetdb"]["db"]["dialect"]), username=config["targetdb"]["db"]["user"], - password=config["targetdb"]["db"]["password"], + password=config["targetdb"]["db"].get("password"), host=config["targetdb"]["db"]["host"], port=config["targetdb"]["db"]["port"], database=config["targetdb"]["db"]["dbname"], @@ -216,21 +256,47 @@ def get_url_object(config): return url_object -def install_q3c_extension(config, dry_run=False): +def get_alembic_url(conf_file=None): + """ + Resolve the database URL used by the alembic ``env.py`` scripts. + + Parameters + ---------- + conf_file : str or pathlib.Path, optional + Path to a targetdb TOML configuration file. When ``None`` the + ``TARGETDB_CONF`` environment variable is consulted instead. + + Returns + ------- + url : str or None + The rendered connection URL, or ``None`` when neither ``conf_file`` nor + ``TARGETDB_CONF`` is set. ``None`` tells the caller to fall back to the + ``sqlalchemy.url`` entry of its own ``alembic.ini``. + + Notes + ----- + This lives in ``targetdb.utils`` rather than in ``env.py`` so that the URL + resolution can be unit tested: ``env.py`` is executed by alembic and is + awkward to import directly. + """ + if conf_file is None: + conf_file = os.environ.get("TARGETDB_CONF") + if not conf_file: + return None + return get_url_object(load_config(conf_file)).render_as_string(hide_password=False) - # connect to the targetDB - db = TargetDB(**config["targetdb"]["db"]) - db.connect() + +def install_q3c_extension(config, dry_run=False): logger.info("Installing q3c extension.") - try: - db.execute_query("CREATE EXTENSION IF NOT EXISTS q3c", dry_run=dry_run) - except Exception as e: - logger.error(f"Error installing q3c extension: {e}") - raise e - # close the connection - db.close() + # connect to the targetDB + with TargetDB(**config["targetdb"]["db"]) as db: + try: + db.execute_query("CREATE EXTENSION IF NOT EXISTS q3c", dry_run=dry_run) + except Exception as e: + logger.error(f"Error installing q3c extension: {e}") + raise e def generate_schema_markdown(output_file=None): @@ -321,7 +387,6 @@ def draw_diagram( f"--database={config['targetdb']['db']['dbname']}", "--schemas=public", f"--user={config['targetdb']['db']['user']}", - f"--password={config['targetdb']['db']['password']}", f"--info-level={sc_info_level}", f"--log-level={sc_log_level}", "--portable-names", @@ -330,14 +395,37 @@ def draw_diagram( f"--output-file={outfile}", "--no-remarks", ] + + # Logged before the password is appended below, so it is not written to + # the log verbatim. It remains visible in the process list while + # SchemaCrawler runs. logger.debug(f"{comm}") + + # `password` is optional in the config file, so it cannot be indexed + # directly -- doing so raised KeyError for password-less configs. + # Omitting --password is fine: SchemaCrawler connects over JDBC, and + # pgjdbc resolves the password from PGPASSFILE, or from ~/.pgpass via + # the JVM's user.home. Note that user.home is not taken from $HOME on + # macOS, so overriding HOME does not redirect the lookup -- use + # PGPASSFILE for that. + password = config["targetdb"]["db"].get("password") + if password is None: + logger.debug( + "No password in the config file; letting SchemaCrawler resolve " + "it from PGPASSFILE or ~/.pgpass." + ) + else: + comm.append(f"--password={password}") + subprocess.run(comm, shell=False) elif generator == "tbls": url_object = get_url_object(config) + # tbls wants a plain `postgres://` DSN with no SQLAlchemy driver + # qualifier. Replacing the scheme on the rendered string would turn + # `postgresql+psycopg://` into `postgres+psycopg://`, which tbls cannot + # parse, so swap the drivername on the URL object instead. url_object_tbls = ( - url_object.render_as_string(hide_password=False).replace( - "postgresql", "postgres", 1 - ) + url_object.set(drivername="postgres").render_as_string(hide_password=False) + "?sslmode=disable" ) @@ -402,7 +490,15 @@ def normalize_filter_columns(df): col = f"filter_{band}" if col not in df.columns: continue - df.loc[df[col].isna() | (df[col] == ""), col] = None + mask = df[col].isna() | (df[col] == "") + # Force object dtype before assigning None: pandas' default string + # dtype (used from pandas 3.0 on, and optionally via + # future.infer_string on 2.x) stores its own NA sentinel and silently + # rewrites an assigned None back to NaN, defeating the point of this + # function -- the caller needs a real None, not a NaN that the + # database driver would coerce to the literal string "NaN". + df[col] = df[col].astype(object) + df.loc[mask, col] = None return df @@ -931,76 +1027,72 @@ def add_database_rows( raise ValueError(f"flux_type must be 'total' or 'psf'. {flux_type=}") logger.info("Connecting to targetDB") - db = TargetDB(**config["targetdb"]["db"]) - db.connect() - - t_begin = time.time() - if table in ["proposal", "fluxstd", "sky", "user_pointing"]: - df = add_backref_values(df, db=db, table=table, upload_id=upload_id) - elif table in ["target"]: - if from_uploader: - df = make_target_df_from_uploader( + # The context manager guarantees close() runs even if the insert/update + # raises, which otherwise leaves pooled connections open and makes a later + # drop-db fail with "database is being accessed by other users". + with TargetDB(**config["targetdb"]["db"]) as db: + t_begin = time.time() + if table in ["proposal", "fluxstd", "sky", "user_pointing"]: + df = add_backref_values(df, db=db, table=table, upload_id=upload_id) + elif table in ["target"]: + if from_uploader: + df = make_target_df_from_uploader( + df, + db=db, + table=table, + proposal_id=proposal_id, + upload_id=upload_id, + flux_type=flux_type, + insert=insert, + update=update, + ) + else: + df = add_backref_values(df, db=db, table=table) + elif table in ["input_catalog"]: + check_input_catalog( df, db=db, - table=table, - proposal_id=proposal_id, - upload_id=upload_id, - flux_type=flux_type, - insert=insert, - update=update, + input_catalog_id_start=input_catalog_id_start, + input_catalog_id_max=input_catalog_id_max, ) - else: - df = add_backref_values(df, db=db, table=table) - elif table in ["input_catalog"]: - check_input_catalog( - df, - db=db, - input_catalog_id_start=input_catalog_id_start, - input_catalog_id_max=input_catalog_id_max, - ) - t_end = time.time() - logger.info(f"Added back reference values in {t_end - t_begin:.2f} s") + t_end = time.time() + logger.info(f"Added back reference values in {t_end - t_begin:.2f} s") - if verbose: - logger.debug(f"{df.columns=}") + if verbose: + logger.debug(f"{df.columns=}") - if verbose: - logger.debug(f"Working on the following DataFrame: \n{df}") + if verbose: + logger.debug(f"Working on the following DataFrame: \n{df}") - try: - t_begin = time.time() - if commit: - dry_run = False - logger.info("Committing the changes to targetDB") - else: - dry_run = True - logger.info("No changes will be committed to targetDB (i.e., dry run)") - - # logger.info(f"{df['total_flux_r'][:100]}") - # db.close() - # exit() - - if insert: - db.insert(table, df, dry_run=dry_run) - elif update: - db.update(table, df, dry_run=dry_run) - t_end = time.time() - logger.info( - f"Insert data to the {table} table successful for {df.index.size} rows in {input_file} ({t_end - t_begin:.2f} s)" - if insert - else f"Update data in the {table} table successful for {df.index.size} rows in {input_file} ({t_end - t_begin:.2f} s)" - ) - except Exception as e: - logger.error(f"Operation failed: {e}: {input_file}") - raise + try: + t_begin = time.time() + if commit: + dry_run = False + logger.info("Committing the changes to targetDB") + else: + dry_run = True + logger.info("No changes will be committed to targetDB (i.e., dry run)") + + if insert: + db.insert(table, df, dry_run=dry_run) + elif update: + db.update(table, df, dry_run=dry_run) + t_end = time.time() + logger.info( + f"Insert data to the {table} table successful for {df.index.size} rows in {input_file} ({t_end - t_begin:.2f} s)" + if insert + else f"Update data in the {table} table successful for {df.index.size} rows in {input_file} ({t_end - t_begin:.2f} s)" + ) + except Exception as e: + logger.error(f"Operation failed: {e}: {input_file}") + raise - if fetch: - logger.info("Fetching the first 100 table entries") - res = db.fetch_all(table) - logger.info(f"Fetched the first 100 entries in the {table} table: \n{res}") + if fetch: + logger.info("Fetching the first 100 table entries") + res = db.fetch_all(table) + logger.info(f"Fetched the first 100 entries in the {table} table: \n{res}") - logger.info("Closing targetDB") - db.close() + logger.info("Closing targetDB") def check_duplicates( @@ -1950,40 +2042,36 @@ def update_input_catalog_active( >>> update_input_catalog_active(123, True, config, commit=True, verbose=True) """ - db = TargetDB(**config["targetdb"]["db"]) - db.connect() - df = pd.DataFrame( {"input_catalog_id": [input_catalog_id], "active": [active_flag]}, # index="input_catalog_id", ) - if verbose: - logger.info( - f"Updating input_catalog_id {input_catalog_id} to active={active_flag}" - ) - df_res = db.fetch_by_id( - "input_catalog", - input_catalog_id=input_catalog_id, - ) - logger.info(f"Original input_catalog table: \n{df_res}") - - if commit: - logger.info( - f"Updating input_catalog_id {input_catalog_id} to active={active_flag}" - ) - else: - logger.info( - f"Updating input_catalog_id {input_catalog_id} to active={active_flag} (dry run)" - ) + with TargetDB(**config["targetdb"]["db"]) as db: + if verbose: + logger.info( + f"Updating input_catalog_id {input_catalog_id} to active={active_flag}" + ) + df_res = db.fetch_by_id( + "input_catalog", + input_catalog_id=input_catalog_id, + ) + logger.info(f"Original input_catalog table: \n{df_res}") - db.update("input_catalog", df, dry_run=not commit) + if commit: + logger.info( + f"Updating input_catalog_id {input_catalog_id} to active={active_flag}" + ) + else: + logger.info( + f"Updating input_catalog_id {input_catalog_id} to active={active_flag} (dry run)" + ) - if verbose: - df_res = db.fetch_by_id( - "input_catalog", - input_catalog_id=input_catalog_id, - ) - logger.info(f"Updated input_catalog table: \n{df_res}") + db.update("input_catalog", df, dry_run=not commit) - db.close() + if verbose: + df_res = db.fetch_by_id( + "input_catalog", + input_catalog_id=input_catalog_id, + ) + logger.info(f"Updated input_catalog table: \n{df_res}") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_alembic.py b/tests/integration/test_alembic.py new file mode 100644 index 0000000..2a0b20b --- /dev/null +++ b/tests/integration/test_alembic.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +Check that the alembic `env.py` scripts build their URL from TARGETDB_CONF. + +Only `alembic/local_test/` is exercised, and only against the throwaway +container -- never a deployment target. `alembic current` is enough: it opens a +connection using the URL `env.py` assembled, which is exactly the part that +changed. No upgrade is attempted. + +These tests assert on the connection, not on alembic's exit status. +`local_test`'s revision history is broken independently of any of this: +`80f8276e2ee7` names a `down_revision` (`ecfad41204d1`) that is not in the +repository, so alembic raises `KeyError` once it walks the revision map -- +after the database connection has already been made. + +The no-TARGETDB_CONF fallback to `alembic.ini` is covered by +`tests/test_targetdb.py::TestGetAlembicUrl`, which needs no server. +""" + +import os +import subprocess +import sys + +import pytest + +from targetdb.utils import load_config + +from .conftest import REPO_ROOT + +ALEMBIC_DIR = REPO_ROOT / "alembic" / "local_test" + + +def run_alembic(*args, env=None): + return subprocess.run( + [sys.executable, "-m", "alembic", *args], + cwd=ALEMBIC_DIR, + env={**os.environ, **(env or {})}, + capture_output=True, + text=True, + ) + + +@pytest.fixture +def alembic_ini_exists(): + if not (ALEMBIC_DIR / "alembic.ini").exists(): + pytest.skip(f"no alembic.ini in {ALEMBIC_DIR}") + + +def test_current_connects_via_targetdb_conf(db_config, schema, alembic_ini_exists): + """env.py must reach the container using only the URL built from the TOML. + + "Context impl PostgresqlImpl" is logged from context.configure(), which + run_migrations_online() only reaches once connectable.connect() has + succeeded -- so it is proof the connection was made. + """ + result = run_alembic("current", env={"TARGETDB_CONF": str(db_config)}) + combined = result.stdout + result.stderr + + assert "Context impl PostgresqlImpl" in combined, combined + for failure in [ + "password authentication failed", + "could not connect", + "OperationalError", + ]: + assert failure not in combined, combined + + +def test_percent_in_password_is_not_interpolated( + db_config, schema, work_dir, alembic_ini_exists +): + """A "%" in the password must reach libpq untouched. + + ConfigParser reads "%" as an interpolation marker, which is why env.py + builds the engine directly instead of going through + config.set_main_option(). The wrong password means the connection is + refused -- the point is *how*: an authentication failure proves the URL was + assembled and handed over intact, whereas an interpolation error would mean + it never got that far. + """ + config = load_config(str(db_config))["targetdb"]["db"] + conf_path = work_dir / "percent_password.toml" + conf_path.write_text( + f"""\ +[targetdb.db] +dialect = "{config["dialect"]}" +user = "{config["user"]}" +password = "pa%%ss" +host = "{config["host"]}" +port = {config["port"]} +dbname = "{config["dbname"]}" +""" + ) + + result = run_alembic("current", env={"TARGETDB_CONF": str(conf_path)}) + combined = result.stdout + result.stderr + + assert result.returncode != 0 + assert "password authentication failed" in combined, combined + assert "Interpolation" not in combined, combined diff --git a/tests/integration/test_dry_run.py b/tests/integration/test_dry_run.py new file mode 100644 index 0000000..f7342ee --- /dev/null +++ b/tests/integration/test_dry_run.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Prove the `--commit` contract against a real PostgreSQL server. + +The CLI defaults to `--commit False`, so every write path has to roll back +unless the flag is passed. Since `TargetDB` moved onto +`pfs.utils.database.db.DB` that guarantee rests on two things only a live +server can demonstrate: + +* `TargetDB.connection()` opens its transaction eagerly, because + `pandas.DataFrame.to_sql` starts *and commits* one of its own when handed a + connection that is not already in a transaction, and +* the `COPY` fast path (`_psql_insert_copy`) writes through a raw psycopg + cursor, so it has to be enrolled in that same transaction to be undone. + +Every test here restores the database before it returns: the fixtures in +conftest.py are session-scoped and other modules assert exact row counts on +them. +""" + +import pandas as pd +import pytest +from sqlalchemy import text + +from targetdb import TargetDB +from targetdb.utils import load_config + +from .conftest import EXAMPLES_DATA, count_rows, run_cli + +PROPOSAL_CSV = EXAMPLES_DATA / "proposals.csv" +# Prefix used by the rows these tests create, so cleanup can find them. +PROBE_PREFIX = "ZZZ-DRYRUN" +TARGET_CATALOG_ID = 1004 + + +@pytest.fixture +def probe_proposals(master_data, engine, tmp_path): + """A CSV of proposals that do not exist yet, removed again afterwards. + + Re-inserting the example rows would fail on the primary key inside COPY + itself, which says nothing about whether the transaction was committed. + """ + df = pd.read_csv(PROPOSAL_CSV).head(2).copy() + df["proposal_id"] = [f"{PROBE_PREFIX}-{i}" for i in range(len(df))] + df["group_id"] = [f"o99{i:03d}" for i in range(len(df))] + csv_path = tmp_path / "probe_proposals.csv" + df.to_csv(csv_path, index=False) + + yield csv_path, len(df) + + with engine.connect() as conn: + conn.execute( + text("DELETE FROM proposal WHERE proposal_id LIKE :p"), + {"p": f"{PROBE_PREFIX}%"}, + ) + conn.commit() + + +def read_active_flag(engine, input_catalog_id): + with engine.connect() as conn: + return conn.execute( + text("SELECT active FROM input_catalog WHERE input_catalog_id = :i"), + {"i": input_catalog_id}, + ).scalar_one() + + +def test_insert_without_commit_writes_nothing(engine, db_config, probe_proposals): + """The raw-cursor COPY write must be rolled back with the transaction.""" + csv_path, _ = probe_proposals + before = count_rows(engine, "proposal") + + run_cli("insert", csv_path, "-c", db_config, "-t", "proposal") + + assert count_rows(engine, "proposal") == before + + +def test_insert_with_commit_writes_rows(engine, db_config, probe_proposals): + csv_path, n_rows = probe_proposals + before = count_rows(engine, "proposal") + + run_cli("insert", csv_path, "-c", db_config, "-t", "proposal", "--commit") + + assert count_rows(engine, "proposal") == before + n_rows + + +def test_update_without_commit_changes_nothing(engine, master_data, db_config): + before = read_active_flag(engine, TARGET_CATALOG_ID) + + run_cli( + "update-catalog-active", TARGET_CATALOG_ID, str(not before), "-c", db_config + ) + + assert read_active_flag(engine, TARGET_CATALOG_ID) == before + + +def test_update_with_commit_changes_the_value(engine, master_data, db_config): + before = read_active_flag(engine, TARGET_CATALOG_ID) + flipped = not before + + run_cli( + "update-catalog-active", + TARGET_CATALOG_ID, + str(flipped), + "-c", + db_config, + "--commit", + ) + assert read_active_flag(engine, TARGET_CATALOG_ID) == flipped + + # Restore, so this module leaves the session fixtures as it found them. + run_cli( + "update-catalog-active", + TARGET_CATALOG_ID, + str(before), + "-c", + db_config, + "--commit", + ) + assert read_active_flag(engine, TARGET_CATALOG_ID) == before + + +def test_execute_query_honours_dry_run(engine, master_data, db_config): + """execute_query() delegates to DB.commit(), which goes through + connection() -- the single place the dry-run decision is made.""" + config = load_config(str(db_config)) + + with engine.connect() as conn: + before = conn.execute( + text( + "SELECT count(*) FROM proposal_category WHERE proposal_category_name = 'dry-run probe'" + ) + ).scalar_one() + assert before == 0 + + with TargetDB(**config["targetdb"]["db"]) as db: + db.execute_query( + "UPDATE proposal_category SET proposal_category_name = 'dry-run probe'", + dry_run=True, + ) + + with engine.connect() as conn: + after = conn.execute( + text( + "SELECT count(*) FROM proposal_category WHERE proposal_category_name = 'dry-run probe'" + ) + ).scalar_one() + assert after == 0 diff --git a/tests/integration/test_targetdb_api.py b/tests/integration/test_targetdb_api.py new file mode 100644 index 0000000..c88a7cd --- /dev/null +++ b/tests/integration/test_targetdb_api.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +Exercise the `TargetDB` public API the way downstream packages call it. + +`ets_pointing` and `pfs_obsproc_planning_tools` use `connect()` / `close()` / +`fetch_query()` / `fetch_all()` / `fetch_by_id()` and discard what `connect()` +returns. Reproducing that shape here keeps the contract under test inside this +repository, without needing either package installed. +""" + +import pandas as pd +import pytest +from sqlalchemy import create_engine, text + +from targetdb import TargetDB, models +from targetdb.utils import get_url_object, load_config + + +@pytest.fixture +def db(db_config, master_data): + config = load_config(str(db_config)) + database = TargetDB(**config["targetdb"]["db"]) + yield database + database.close() + + +def test_downstream_call_sequence(db): + """The exact shape used in the downstream dbutils.py modules.""" + db.connect() # return value deliberately discarded + df = db.fetch_query("SELECT * FROM proposal") + assert isinstance(df, pd.DataFrame) + assert not df.empty + db.close() + + +def test_connect_does_not_leak_a_connection(db): + """DB.connect() checks a connection out of the pool and hands it over; the + override must not, since every caller throws the return value away.""" + db.connect() + assert db.engine.pool.checkedout() == 0 + + +def test_query_methods_return_connections_to_the_pool(db): + db.connect() + db.fetch_query("SELECT 1") + db.fetch_all("proposal") + db.fetch_by_id("proposal", proposal_id="S21B-EN01") + assert db.engine.pool.checkedout() == 0 + + +def test_close_releases_server_side_connections(db, db_config): + """Without this, `drop-db` fails with "database is being accessed by other + users".""" + db.connect() + db.fetch_query("SELECT 1") + + config = load_config(str(db_config)) + dbname = config["targetdb"]["db"]["dbname"] + # A separate engine, so counting does not observe its own connection as one + # of TargetDB's. + observer = create_engine(get_url_object(config)) + try: + with observer.connect() as conn: + before = conn.execute( + text( + "SELECT count(*) FROM pg_stat_activity " + "WHERE datname = :d AND pid <> pg_backend_pid()" + ), + {"d": dbname}, + ).scalar_one() + assert before >= 1 + + db.close() + + with observer.connect() as conn: + after = conn.execute( + text( + "SELECT count(*) FROM pg_stat_activity " + "WHERE datname = :d AND pid <> pg_backend_pid()" + ), + {"d": dbname}, + ).scalar_one() + assert after == before - 1 + finally: + observer.dispose() + + +def test_engine_is_reusable_after_close(db): + """Engine.dispose() replaces the pool rather than invalidating the engine, + so the instance cached in pfs.utils.database.db._DB_ENGINES stays usable.""" + db.connect() + db.close() + assert not db.fetch_all("proposal").empty + + +def test_fetch_all_column_names_and_order_match_the_model(db): + """join_backref_values() merges these results into user DataFrames, so a + renamed or reordered column would break things silently.""" + df = db.fetch_all("proposal") + expected = list(models.proposal.__table__.columns.keys()) + assert list(df.columns) == expected + + +@pytest.mark.parametrize("table", ["proposal", "input_catalog", "target_type"]) +def test_fetch_all_matches_a_direct_count(db, engine, table): + from .conftest import count_rows + + assert len(db.fetch_all(table)) == count_rows(engine, table) + + +def test_fetch_by_id_filters_on_every_keyword(db): + df = db.fetch_by_id("proposal", proposal_id="S21B-EN01") + assert len(df) == 1 + assert df["proposal_id"].iloc[0] == "S21B-EN01" + + assert db.fetch_by_id("proposal", proposal_id="does-not-exist").empty + + # Two keywords must be ANDed, not the last one winning. + assert db.fetch_by_id( + "proposal", proposal_id="S21B-EN01", pi_last_name="Nobody" + ).empty + + +def test_fetch_by_id_columns_match_fetch_all(db): + assert list(db.fetch_by_id("proposal", proposal_id="S21B-EN01").columns) == list( + db.fetch_all("proposal").columns + ) + + +def test_context_manager_closes_the_engine(db_config, master_data): + config = load_config(str(db_config)) + with TargetDB(**config["targetdb"]["db"]) as database: + assert not database.fetch_all("proposal").empty + assert database.engine.pool.checkedout() == 0 diff --git a/tests/test_targetdb.py b/tests/test_targetdb.py new file mode 100644 index 0000000..d450fa5 --- /dev/null +++ b/tests/test_targetdb.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +Unit tests for `TargetDB` and the URL helpers it shares with `targetdb.utils`. + +None of these need a database: URL construction is a pure function of the +configuration, and `connect()` only materializes a lazily-created engine. +The behaviour that does need a live server is covered by +`tests/integration/test_targetdb_api.py` and `tests/integration/test_dry_run.py`. +""" + +import pytest + +from targetdb import TargetDB +from targetdb.utils import get_alembic_url, get_url_object, normalize_drivername + +# Methods dropped when TargetDB became a subclass of pfs.utils.database.db.DB. +# reset_* stopped working under SQLAlchemy 2.x and *_by_copy used psycopg2-only +# APIs; none of the three PFS repositories called any of them. +REMOVED_METHODS = [ + "reset_all", + "reset_target", + "reset_fluxstd", + "reset_sky", + "insert_by_copy", + "fetch_by_copy", + "insert_mappings", + "rollback", +] + + +def make_config(**overrides): + db = { + "dialect": "postgresql", + "user": "admin", + "password": "secret", + "host": "db.example.org", + "port": 5433, + "dbname": "targetdb", + } + db.update(overrides) + for key in [k for k, v in db.items() if v is None]: + del db[key] + return {"targetdb": {"db": db}} + + +class TestNormalizeDrivername: + @pytest.mark.parametrize("dialect", ["postgresql", "postgres"]) + def test_bare_postgres_names_map_to_psycopg3(self, dialect): + assert normalize_drivername(dialect) == "postgresql+psycopg" + + @pytest.mark.parametrize( + "dialect", ["postgresql+psycopg", "postgresql+psycopg2", "mysql+pymysql"] + ) + def test_explicit_driver_is_passed_through(self, dialect): + assert normalize_drivername(dialect) == dialect + + +class TestUrl: + def test_url_contains_password_and_psycopg3_driver(self): + db = TargetDB(**make_config()["targetdb"]["db"]) + assert ( + db.url == "postgresql+psycopg://admin:secret@db.example.org:5433/targetdb" + ) + + def test_url_omits_password_when_not_configured(self): + """Without a password the URL matches DB.url's native shape, so libpq + resolves the credential itself via PGPASSWORD / ~/.pgpass.""" + db = TargetDB(**make_config(password=None)["targetdb"]["db"]) + assert db.url == "postgresql+psycopg://admin@db.example.org:5433/targetdb" + assert "secret" not in db.url + + def test_password_special_characters_are_escaped(self): + db = TargetDB(**make_config(password="p%w@rd")["targetdb"]["db"]) + assert db.url == ( + "postgresql+psycopg://admin:p%25w%40rd@db.example.org:5433/targetdb" + ) + + def test_explicit_dialect_is_honoured(self): + db = TargetDB(**make_config(dialect="postgresql+psycopg")["targetdb"]["db"]) + assert db.url.startswith("postgresql+psycopg://") + + +class TestGetUrlObject: + def test_applies_the_same_driver_normalization(self): + url = get_url_object(make_config()) + assert url.drivername == "postgresql+psycopg" + + def test_password_is_optional(self): + url = get_url_object(make_config(password=None)) + assert url.password is None + assert url.render_as_string(hide_password=False) == ( + "postgresql+psycopg://admin@db.example.org:5433/targetdb" + ) + + def test_matches_targetdb_url(self): + config = make_config() + url = get_url_object(config).render_as_string(hide_password=False) + assert url == TargetDB(**config["targetdb"]["db"]).url + + +class TestClassDefaults: + """DB.__init__ resolves omitted parameters from type(self).DEFAULT_*. + + These class attributes are the only thing that makes ``TargetDB()`` work + bare, and the only thing that makes the inherited + ``set_default_connection()`` classmethod do anything -- it was a silent + no-op while ``__init__`` carried non-None defaults in its own signature. + """ + + def test_bare_construction_uses_the_class_defaults(self): + db = TargetDB() + assert db.url == "postgresql+psycopg://obsproc@pfsa-db:5433/targetdb" + + def test_the_default_user_is_the_read_only_one(self): + """Bare TargetDB() must not be able to write to production: every + write path passes an explicit user from a config file.""" + assert TargetDB.DEFAULT_USER == "obsproc" + + @pytest.mark.parametrize("missing", ["host", "port", "dbname", "user"]) + def test_omitted_parameter_falls_back_to_its_default(self, missing): + db = TargetDB(**make_config(**{missing: None})["targetdb"]["db"]) + assert getattr(db, missing) == getattr(TargetDB, f"DEFAULT_{missing.upper()}") + + def test_explicit_values_win_over_the_defaults(self): + db = TargetDB(**make_config()["targetdb"]["db"]) + assert (db.host, db.user, db.dbname) == ( + "db.example.org", + "admin", + "targetdb", + ) + + def test_omitted_dialect_uses_the_default(self): + db = TargetDB(**make_config(dialect=None)["targetdb"]["db"]) + assert db.url.startswith("postgresql+psycopg://") + + def test_set_default_connection_is_honoured(self, monkeypatch): + # Setting each attribute to its current value first registers the + # undo, so the class-level mutation below does not leak into other + # tests. + monkeypatch.setattr(TargetDB, "DEFAULT_HOST", TargetDB.DEFAULT_HOST) + monkeypatch.setattr(TargetDB, "DEFAULT_PORT", TargetDB.DEFAULT_PORT) + TargetDB.set_default_connection(host="pfsa-db01-gb", port=5555) + db = TargetDB() + assert (db.host, db.port) == ("pfsa-db01-gb", 5555) + + def test_missing_password_does_not_raise(self): + TargetDB(**make_config(password=None)["targetdb"]["db"]) + + +class TestApiSurface: + def test_connect_returns_none(self): + """DB.connect() hands back a pooled Connection, but every caller here + and downstream discards the return value, which would leak one + checked-out connection per call. The override must return nothing.""" + db = TargetDB(**make_config()["targetdb"]["db"]) + assert db.connect() is None + + @pytest.mark.parametrize("name", REMOVED_METHODS) + def test_removed_methods_are_gone(self, name): + db = TargetDB(**make_config()["targetdb"]["db"]) + assert not hasattr(db, name) + + @pytest.mark.parametrize( + "name", ["connect", "close", "fetch_query", "fetch_all", "fetch_by_id"] + ) + def test_downstream_methods_are_kept(self, name): + db = TargetDB(**make_config()["targetdb"]["db"]) + assert callable(getattr(db, name)) + + +class TestGetAlembicUrl: + def _write_config(self, tmp_path, password="secret"): + conf = tmp_path / "dbconf.toml" + conf.write_text( + f"""\ +[targetdb.db] +dialect = "postgresql" +user = "admin" +password = "{password}" +host = "db.example.org" +port = 5433 +dbname = "targetdb" +""" + ) + return conf + + def test_reads_the_file_named_by_targetdb_conf(self, tmp_path, monkeypatch): + monkeypatch.setenv("TARGETDB_CONF", str(self._write_config(tmp_path))) + assert get_alembic_url() == ( + "postgresql+psycopg://admin:secret@db.example.org:5433/targetdb" + ) + + def test_returns_none_without_targetdb_conf(self, monkeypatch): + """None is the signal for env.py to fall back to alembic.ini.""" + monkeypatch.delenv("TARGETDB_CONF", raising=False) + assert get_alembic_url() is None + + def test_explicit_argument_wins_over_the_environment(self, tmp_path, monkeypatch): + monkeypatch.setenv("TARGETDB_CONF", "/nonexistent/does-not-exist.toml") + conf = self._write_config(tmp_path) + assert "db.example.org" in get_alembic_url(conf_file=conf) + + def test_percent_in_password_survives(self, tmp_path, monkeypatch): + """ConfigParser would treat "%" as an interpolation marker, which is why + env.py builds the engine directly instead of calling + config.set_main_option().""" + monkeypatch.setenv( + "TARGETDB_CONF", str(self._write_config(tmp_path, password="pa%%ss")) + ) + assert get_alembic_url().endswith("@db.example.org:5433/targetdb") + assert "pa%25%25ss" in get_alembic_url() diff --git a/uv.lock b/uv.lock index 34401b5..6716197 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.11, <3.13" -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", -] +requires-python = "==3.12.*" [[package]] name = "alembic" @@ -20,6 +16,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/fd/68773667babd452fb48f974c4c1f6e6852c6e41bcf622c745faca1b06605/alembic-1.18.0-py3-none-any.whl", hash = "sha256:3993fcfbc371aa80cdcf13f928b7da21b1c9f783c914f03c3c6375f58efd9250", size = 260967, upload-time = "2026-01-09T21:22:25.333Z" }, ] +[[package]] +name = "astroplan" +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astropy" }, + { name = "numpy" }, + { name = "pytz" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/80/1be3af06717245a51adaf6994b682b3ee25ace9bfe5d443d440209d627df/astroplan-0.10.1.tar.gz", hash = "sha256:39d97c3377e1630abff3a94d8c956980f77a3e809e27a0376dd7d30abe3b6959", size = 140603, upload-time = "2024-08-13T20:12:04.462Z" } + [[package]] name = "astropy" version = "7.2.0" @@ -96,11 +104,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" }, - { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" }, - { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" }, - { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" }, - { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" }, { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, @@ -155,19 +158,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -197,22 +187,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, @@ -253,27 +227,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, +] + [[package]] name = "coverage" version = "7.15.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f4/45/78dbf9604ee5b3db24efbf26bed1cb58862fb40480cba821963c69348751/coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d", size = 935592, upload-time = "2026-08-02T18:50:17.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/9c/c8a3a923c24f631695cea2d5e2f02e776bc0af6e03800626e13a6c05a615/coverage-7.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f3f854ab4599d98f7799ac9b91e34e8ec9ebc9a6372ee8c1f3413a68cc8b5e9", size = 222328, upload-time = "2026-08-02T18:47:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/92/51/dda77f34cbd2513d6ffb898c901d19e9ca55f48c0cbc4a1eb173a97d157a/coverage-7.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75268348fee1f199653b8a846262aec5581c6bb008c4f58824959fb708cc688f", size = 222832, upload-time = "2026-08-02T18:47:51.219Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/e0faafc4c6e23bd76c76148875ee9ec5781b8f1cd62cea2bc4ca0f0f0e5d/coverage-7.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21081739f6264cc594cad2d42b62befbd17633824022866c68720eb0c4b8d6b4", size = 253250, upload-time = "2026-08-02T18:47:52.737Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/4b1e0eeb727ffb471e411c1bd3402184b5dd54a77a762b0e55e87cdf9ae3/coverage-7.15.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:718d366251b060c10731c7dd359de6caea72250036eb94576aa56dacbf830a11", size = 255160, upload-time = "2026-08-02T18:47:54.404Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/a602d2d48f9db9f795e578a86aa914f7b20008e9330902defcfb73d17b3a/coverage-7.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1bbaa502a6e877f3ee67cbac3eba2bb637f623e454e6c37b81b38896dbd48f", size = 257269, upload-time = "2026-08-02T18:47:56.157Z" }, - { url = "https://files.pythonhosted.org/packages/22/fa/bf6db13df2fcee00d2671849fe58c99232ee79a01fec7478c2bf7839b9e1/coverage-7.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:494880c9e60782610683f4eb9b65cce4f886673596b8f3cb2dfa079fc551c743", size = 259231, upload-time = "2026-08-02T18:47:57.76Z" }, - { url = "https://files.pythonhosted.org/packages/89/37/8118f13b17fa7d9a3aa2c301d93f2d5ffeef70fa7e27e639a74bdacd3fea/coverage-7.15.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3db264ea689f9e8f9fa4fb9005fee4048c3bff4a547f4cfa27f5086cb0804ec0", size = 253357, upload-time = "2026-08-02T18:47:59.261Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/c7b94fb03962f4d6f0fe13d01c4eb9c4c6e2e714a20d074516ec7582b110/coverage-7.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e869d4799674d67778e76ddbe2e26cf1673369262e231a8ec259421b1015fea", size = 254961, upload-time = "2026-08-02T18:48:00.901Z" }, - { url = "https://files.pythonhosted.org/packages/87/f9/fe0bd415fa56e36b62b649017c8fc98330858be4c7593789efb78cd24178/coverage-7.15.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:696fc7a28bbf717aba8d2c6963d26702945c7832cb313ba3b323aa5b1afb3156", size = 253024, upload-time = "2026-08-02T18:48:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/c1/7c/ffa53506d63ba8a77f5b9557dd6f5a5a5ad85adc680d7857410138f82bd9/coverage-7.15.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3fe9be1c527497d047f770d88a0110189714c36383bb88384508f750c302bffa", size = 256792, upload-time = "2026-08-02T18:48:04.377Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c6/df42458e72c18a49fe87e40ccd3fb0314210915256cf4a5593e1b3250e04/coverage-7.15.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2400591f4b2e33746c70846388f8bb4c7e33b820e31cb8c6cb2f25305310438b", size = 252744, upload-time = "2026-08-02T18:48:06.154Z" }, - { url = "https://files.pythonhosted.org/packages/f1/14/8bf18a4b10a44f8ba5f604b00e102f37daf49d581d66a37dc33fa267e1a6/coverage-7.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e557178799282269412a672e5753f2179edfe1b3f0f19b0c98f8e72d482326a", size = 253652, upload-time = "2026-08-02T18:48:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/e530c9bb94e4155817cbd149034105b062a6913bc356ae08f454d155de53/coverage-7.15.3-cp311-cp311-win32.whl", hash = "sha256:68ea6c947375982ae907e19e9d2ef156bd6e68e11f3566dd568d7f4ec974e715", size = 224428, upload-time = "2026-08-02T18:48:09.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/98/0050c692d120988f1973a15196f52dee4ae221848b760281461a2005b613/coverage-7.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:28743dad31622e8c474b17446118037361f5b1f4f2ecdf72d4f6fde246d64446", size = 224906, upload-time = "2026-08-02T18:48:11.611Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ae/c0ef3e2ba3f35fc1c6985811a40edd9331e5b8978c9ecf84699de3edacbe/coverage-7.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:c4398918c4fda32718191239e451fd86ac5ad1e8979b592f1921ee2d1f038965", size = 224448, upload-time = "2026-08-02T18:48:13.304Z" }, { url = "https://files.pythonhosted.org/packages/d1/6c/bac99d9d4c6abe856e93bf3f5212982ac0bfac126dd4a042753bd53bc5af/coverage-7.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79a3e32e83227d83d9684459ed579769b56c369ac2d7313099b2d9e031d2e10f", size = 222499, upload-time = "2026-08-02T18:48:15.018Z" }, { url = "https://files.pythonhosted.org/packages/aa/bc/cb9a39b083bc1aa70586482dab25c9be20bab0ec6c155340e50d9066bb1e/coverage-7.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:767feb87c5886d781d0a69fafd450a20826ddab7b79bce1665deb64d21441b60", size = 222866, upload-time = "2026-08-02T18:48:16.884Z" }, { url = "https://files.pythonhosted.org/packages/58/fb/beaa453d62000a0a5b39838bee2a137afe609a50a71f55e83c73461e513b/coverage-7.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50951e37033c40548d777b8a8454a2cd622dba1136780065678dccaec307c47f", size = 254367, upload-time = "2026-08-02T18:48:18.507Z" }, @@ -292,11 +273,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/e7/7069b3d6c018917f49ba2e1c5fb910e498c7fefa3a1b78cb1b79e61ff45d/coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e", size = 214297, upload-time = "2026-08-02T18:50:14.709Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cssselect2" version = "0.8.0" @@ -310,6 +286,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -328,6 +313,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + [[package]] name = "distlib" version = "0.4.3" @@ -356,7 +353,8 @@ dependencies = [ { name = "numpy" }, { name = "openpyxl" }, { name = "pandas" }, - { name = "psycopg2-binary" }, + { name = "pfs-utils" }, + { name = "psycopg", extra = ["binary"] }, { name = "pyarrow" }, { name = "requests" }, { name = "sqlalchemy" }, @@ -392,8 +390,9 @@ requires-dist = [ { name = "numpy", specifier = ">=2.0" }, { name = "openpyxl" }, { name = "pandas" }, + { name = "pfs-utils", git = "https://github.com/Subaru-PFS/pfs_utils.git?rev=master" }, { name = "pre-commit", marker = "extra == 'dev'" }, - { name = "psycopg2-binary" }, + { name = "psycopg", extras = ["binary"] }, { name = "pyarrow" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, @@ -403,7 +402,6 @@ requires-dist = [ { name = "sqlalchemy" }, { name = "sqlalchemy-utils" }, { name = "tabulate" }, - { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" }, { name = "typer" }, ] provides-extras = ["dev", "doc"] @@ -426,6 +424,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, ] +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -444,13 +459,6 @@ version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, @@ -502,7 +510,6 @@ dependencies = [ { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } wheels = [ @@ -545,6 +552,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -558,6 +592,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] +[[package]] +name = "lsst-utils" +version = "30.2026.2900" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astropy" }, + { name = "deprecated" }, + { name = "numpy" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "structlog" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/b6/3198405f2110508a76d918b75de8a82a5ffa858ce4813f49d877b6d122a7/lsst_utils-30.2026.2900.tar.gz", hash = "sha256:7ee345e1df58030e061c78a3ae5a0f018b6a5c330a4a3f2bcac5449388c137b8", size = 94768, upload-time = "2026-07-16T08:28:50.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/e1/31ad285a8e92a1d246672e110f79d513114d30aab06a0ef826b09574fa65/lsst_utils-30.2026.2900-py3-none-any.whl", hash = "sha256:df6ebc7c65121547b239ea8734e8585465da21389cbd2fdff87cd65e673f90ff", size = 76024, upload-time = "2026-07-16T08:28:49.375Z" }, +] + [[package]] name = "mako" version = "1.3.10" @@ -597,17 +649,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -621,6 +662,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, +] + [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -750,17 +817,6 @@ version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166, upload-time = "2025-12-20T16:15:43.434Z" }, - { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781, upload-time = "2025-12-20T16:15:45.701Z" }, - { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247, upload-time = "2025-12-20T16:15:47.943Z" }, - { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807, upload-time = "2025-12-20T16:15:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992, upload-time = "2025-12-20T16:15:51.615Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871, upload-time = "2025-12-20T16:15:54.129Z" }, - { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190, upload-time = "2025-12-20T16:15:56.147Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762, upload-time = "2025-12-20T16:15:58.524Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359, upload-time = "2025-12-20T16:16:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132, upload-time = "2025-12-20T16:16:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977, upload-time = "2025-12-20T16:16:04.77Z" }, { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" }, { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" }, { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" }, @@ -772,13 +828,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" }, { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" }, { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003, upload-time = "2025-12-20T16:18:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105, upload-time = "2025-12-20T16:18:05.594Z" }, - { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590, upload-time = "2025-12-20T16:18:08.031Z" }, - { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947, upload-time = "2025-12-20T16:18:09.836Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119, upload-time = "2025-12-20T16:18:12.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815, upload-time = "2025-12-20T16:18:14.433Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376, upload-time = "2025-12-20T16:18:16.524Z" }, ] [[package]] @@ -823,13 +872,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, @@ -869,23 +911,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pfs-datamodel" +version = "0.0.0" +source = { git = "https://github.com/Subaru-PFS/datamodel.git#24aa6992d5f30599324b3df60e2b3224201021ee" } +dependencies = [ + { name = "astroplan" }, + { name = "astropy" }, + { name = "lsst-utils" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pytz" }, + { name = "pyyaml" }, + { name = "scipy" }, +] + +[[package]] +name = "pfs-instdata" +version = "1.8.88" +source = { git = "https://github.com/Subaru-PFS/pfs_instdata.git#434af74a3fb5a1a7d96fd24abfc65da442465091" } + +[[package]] +name = "pfs-utils" +version = "7.2026.3100" +source = { git = "https://github.com/Subaru-PFS/pfs_utils.git?rev=master#441699f826ef4edc6d203ee91a4aeb05b9f659e0" } +dependencies = [ + { name = "astroplan" }, + { name = "astropy" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pfs-datamodel" }, + { name = "pfs-instdata" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pytz" }, + { name = "scipy" }, + { name = "sqlalchemy" }, +] + [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, @@ -897,13 +967,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] @@ -953,33 +1016,55 @@ wheels = [ ] [[package]] -name = "psycopg2-binary" -version = "2.9.11" +name = "psutil" +version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, - { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, - { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, - { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, - { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, - { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, - { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, - { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/7d/03818e13ba7f36de93573c93ee3482006d3dfa8b0f8d28df511bad0a1a92/psycopg_binary-3.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089", size = 4591122, upload-time = "2026-05-01T23:27:56.162Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/11b341edf8d54e2694726b273fe9652b254d989f4f63e3ac6816ad6b55f4/psycopg_binary-3.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578", size = 4669943, upload-time = "2026-05-01T23:28:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/18/4665bacd65e7865b4372fcd8abb8b9186ada4b0025f8c2ca691b364a556c/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9", size = 5469697, upload-time = "2026-05-01T23:28:11.337Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b83136c6e510593d9b0c759ba5384337bc4ad82d19fda675adc4b2703c84/psycopg_binary-3.3.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d", size = 5152995, upload-time = "2026-05-01T23:28:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/67/8d/a9821e2a648afe6091989929982a3b0f00b2631a859cb81379728f08fb75/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b", size = 6738180, upload-time = "2026-05-01T23:28:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/7e/58/2e349e8d23905dc2317b80ac65f48fb6f821a4777a4e994a60da91c4850f/psycopg_binary-3.3.4-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070", size = 4978828, upload-time = "2026-05-01T23:28:37.277Z" }, + { url = "https://files.pythonhosted.org/packages/45/48/57b00d03b4721878326122a1f1e6b0a90b85bcaec56b5b2f8ea6cfa45235/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68", size = 4509757, upload-time = "2026-05-01T23:28:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/37/33b47d8c007df69aec500df5889767c4d313748e8e9e27a2fef8a6dabcee/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16", size = 4190546, upload-time = "2026-05-01T23:28:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c6/32b0835dbc2122617902b649d76a91c1e75406e76bf3d595b0c3bb5ffad6/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652", size = 3926197, upload-time = "2026-05-01T23:28:55.55Z" }, + { url = "https://files.pythonhosted.org/packages/cd/68/d190ef0c0c5b16ded07831dabc8ddd412f4cdab07ec6e30ed38d9bda0e1f/psycopg_binary-3.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e", size = 4236627, upload-time = "2026-05-01T23:29:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/81dcbc2e8454b74d14881275ea45f00791052dac531a9fa8be1730d1685b/psycopg_binary-3.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4", size = 3560782, upload-time = "2026-05-01T23:29:11.967Z" }, ] [[package]] @@ -1006,13 +1091,6 @@ version = "22.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, @@ -1071,6 +1149,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -1092,7 +1179,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -1149,15 +1236,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -1236,6 +1314,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, ] +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -1286,13 +1385,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, @@ -1328,6 +1420,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "structlog" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/89/b4a0bcfdf4f71a3dea31379f095929613d7e4528a0996bca6aa964cd0dca/structlog-26.1.0.tar.gz", hash = "sha256:f63a716cbd1b1291cf7661de7794b455acfa4c43c5bcf1630e6ad5ddc1adb3b7", size = 1459881, upload-time = "2026-06-06T07:33:39.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" }, +] + [[package]] name = "tabulate" version = "0.9.0" @@ -1337,6 +1438,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "tinycss2" version = "1.5.1" @@ -1349,33 +1459,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - [[package]] name = "traitlets" version = "5.14.3" @@ -1448,9 +1531,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, @@ -1492,3 +1572,23 @@ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b66 wheels = [ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, ] + +[[package]] +name = "wrapt" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, + { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, + { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +]