Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
06fd592
moving files around
thecaffiend Mar 4, 2026
dfc0eba
changed to support custom cli option for db_url. removed bypass of al…
thecaffiend Mar 4, 2026
034bdc5
fixed some order of operations and removed basic config from logging
thecaffiend Mar 4, 2026
8a9bfba
changed to use config from alembic setup
thecaffiend Mar 4, 2026
05a841a
packages
thecaffiend Mar 4, 2026
623f7b4
removed old includes and added new cli script pathing
thecaffiend Mar 4, 2026
43912ef
changed 'alembic' package name (and obvi dir) to 'migrations' so as t…
thecaffiend Mar 5, 2026
7827a3d
changed 'alembic' import to 'migrations'. typo fix
thecaffiend Mar 5, 2026
632f55e
changed script_location to reflect change of 'alembic'->'migrations'
thecaffiend Mar 5, 2026
c54988a
added TODO about needing to change the db_url configuration when we s…
thecaffiend Mar 5, 2026
cad38fa
import order and logger changes.
thecaffiend Mar 5, 2026
04285c9
cleaned up app.py and added a warning to it.
thecaffiend Mar 5, 2026
15a07ef
handled case where we're not using alembic stuff and the config is no…
thecaffiend Mar 5, 2026
e660f6d
updated with better doc for capedb and capedb-app scripts, as well as…
thecaffiend Mar 5, 2026
37b6824
added note about requiring the database to exist to apply migrations …
thecaffiend Mar 5, 2026
71b5432
fixed dependencies so they install correctly in non-dev setups
thecaffiend Mar 5, 2026
89d88b8
typo
thecaffiend Mar 5, 2026
e19d495
changed black formatting ignore for new location of alembic generated…
thecaffiend Mar 5, 2026
e4a4e5e
changed black formatting ignore for new location of alembic generated…
thecaffiend Mar 5, 2026
38d1819
updated lock file for move from deve to real depends
thecaffiend Mar 5, 2026
d20da82
pyright config for change from alembic->migrations
thecaffiend Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 99 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,69 @@ requirements are as follows:

### Setup DB

Make sure the env file contains a valid DB_URL for your setup. If you're
following this setup exactly, the value is already correct
(`DB_URL="postgresql:///cape_env_db"`).

**_NOTE:_** You can also specify this in an environment variable name `DB_URL`.
This is intended more for automation use cases where the installation of the
package is less controllable.

This package provides the `capedb` script to handle DB upgrades, downgrades and
checking of current version. All other `alembic` commands (including upgrades,
downgrades and checking of current version) can be accomplished via normal
`alembic` means.

**_NOTE:_** At this point we expect an empty database to exist before we apply
migrations or create tables. This could change in the future, but for now it's a
requirement. If you followed the postgres setup above you have already done
this.

#### Alembic Config

Usage of the `capedb` script requires an `alembic` config file be available.
This can be accomplished in a number of ways:

- have it available in the current working directory when executing the `capedb`
script (it will be found automatically)
- specification on the command line when calling `capedb`:
`capedb [-c | --config <CONFIG_PATH>]`
- specifying the location in the `ALEMBIC_CONFIG` environment variable

**_NOTE:_** However the `alembic.ini` is specified, the value of
`script_location` needs to point to the migrations directory of this package.
This is most likely `<python_site_package_dir>/cape_cod_db/migrations` if you
are using an installed version of this package. The
`<python_site_package_dir>/cape_cod_db` directory also contains an `alembic.ini`
that can be used if defaults are fine, though you will want to specify the
database url as detailed below if you use the builtin config file.

#### Database URL

Though the database url is often defined in the `alembic` config, if you also
need to specify it separately you also have a few options:

- specification on the command line when calling `capedb`:
`capedb [-x db_url=<DB_URL>]`
- specifying the location in the `DB_URL` environment variable

#### capedb Script Usage

Run the alembic migrations on the empty (or previously upgraded) db to get up to
date `capedb upgrade head`. _NOTE:_ `head` can be replaced with another revision
identifier to go to a specific version.

Downgrades can be performed via `capedb upgrade <revision>`.
Downgrades can be performed via `capedb downgrade <revision>`.

The current version can be determined with `capedb current [--verbose]`.

#### capedb-app script

This is probably not anything you want to use. Probably.

This is the more traditional `app.py` like mechanism to create database tables.
This is not compatible with our provided migrations either. If you wish to
create an empty database using only the most schema in this version of this
package without any migration information, this is your boo. But you would have
to maintain your own migrations at that point. If you are maintaining migrations
ever.

The current version can be determined with `capedb current [--verbose]`
This script requires the `DB_URL` environment variable be set and does not work
with alembic at all, so the alembic config is not needed.

### play with it
### Play With The DB

From repo root in the python repl of your fancy (that has all the dependencies
installed).
Expand Down Expand Up @@ -139,3 +180,50 @@ with Session(db.engine) as session:
for u in res.all():
print(u)
```

If you prefer `psql` (**_NOTE:_** this assumes you have all the perms on the
db):

```bash
12:12 $ psql cape_env_db


psql (18.3)
Type "help" for help.

cape_env_db=> \d+
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+-----------------+----------+-------+-------------+---------------+------------+-------------
public | alembic_version | table | xxxx | permanent | heap | 8192 bytes |
public | user | table | xxxx | permanent | heap | 16 kB |
public | user_id_seq | sequence | xxxx | permanent | | 8192 bytes |

cape_env_db=> \d user
Table "public.user"
Column | Type | Collation | Nullable | Default
-------------+-----------------------------+-----------+----------+----------------------------------
created_at | timestamp without time zone | | not null |
last_edited | timestamp without time zone | | not null |
id | integer | | not null | nextval('user_id_seq'::regclass)
first_name | character varying | | not null |
last_name | character varying | | not null |
email | character varying | | not null |

cape_env_db=> select * from alembic_version;
version_num
--------------
6001985fea71
(1 row)

# NOTE that if you manipulate records via sql, the created_at and last_edited
# **ARE NOT HANDLED FOR YOU**

cape_env_db=> insert into public.user (created_at, last_edited, first_name, last_name, email) values (now(), now(), 'First', 'Last', 'fl@fakeemail.test');
INSERT 0 1
cape_env_db=> select * from public.user;
created_at | last_edited | id | first_name | last_name | email
----------------------------+----------------------------+----+------------+-----------+-------------------
2026-03-05 13:55:21.688756 | 2026-03-05 13:55:21.688756 | 2 | First | Last | fl@fakeemail.test
(1 row)
```
15 changes: 0 additions & 15 deletions app.py

This file was deleted.

2 changes: 1 addition & 1 deletion alembic.ini → cape_cod_db/alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
script_location = %(here)s/migrations

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
Expand Down
Empty file added cape_cod_db/cli/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions cape_cod_db/cli/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from cape_cod_db.database import create_tables
from cape_cod_db.models import User

# WARNING: Usage of this script only applies the most recent schema, with 0
# respect to migrations and without including any migration tables or
# history. If migrations are desired (probable) use `capedb.py`
# instead.


def main():
create_tables()


if __name__ == "__main__":
main()
File renamed without changes.
41 changes: 31 additions & 10 deletions cape_cod_db/database.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
import logging

from dotenv import dotenv_values
from sqlmodel import SQLModel, create_engine

config = dotenv_values(".env")
db_url = None

logging.basicConfig()
logger = logging.getLogger("sqlalchemy.engine")
log_level = config.get("LOG_LEVEL", logging.INFO)
assert log_level is not None, "Log level should not be None"
logger.setLevel(log_level)
try:
from .migrations.env import config

logger = logging.getLogger("alembic.env")

db_url = config.get("DB_URL", None)
# this works for the case where we're doing alembic things. When we're
# doing orm things (e.g. in an api lambda) we need another source to check
# some other source for the DB URL as the config object doesn't exit in
# env.py. alembic doesn't play when we're just doing orm things.
db_url = config.get_main_option("sqlalchemy.url")
except AttributeError as ae:
logging.basicConfig()
logger = logging.getLogger(__file__)
logger.warning(
"Not running with a valid alembic config. Checking for DB_URL "
"environment variable"
)

import os

db_url = os.getenv("DB_URL")

if db_url is None:
logger.error("DB_URL is not configured")
logger.error(
"DB_URL is not configured. This needs to be in the alembic config "
"file or an environment variable named `DB_URL`"
)
exit(1)

logger.info(f"Configured for database: {db_url}")

engine = create_engine(db_url)


def create_db_and_tables():
def create_tables():
"""Create the tables on the DB pointed to by `engine`.

At this point we expect the empty database to exist when calling this.
"""
SQLModel.metadata.create_all(engine)
File renamed without changes.
4 changes: 4 additions & 0 deletions cape_cod_db/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from pathlib import Path

# export of the default alembic config installed with this package
DEFAULT_ALEMBIC_CFGPATH = Path(__file__).parent.parent / "alembic.ini"
56 changes: 21 additions & 35 deletions alembic/env.py → cape_cod_db/migrations/env.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import logging
import os
import sys

# so that alembic has access to our models, we're adding the project root here
proj_root = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, proj_root)

print(sys.path)

from logging.config import fileConfig

from dotenv import dotenv_values
from alembic import context
from sqlalchemy import engine_from_config, pool
from sqlmodel import SQLModel

from alembic import context

# NOTE: as new table models are added, they need to be imported here.
from cape_cod_db.models import User

Expand All @@ -27,43 +18,38 @@
if config.config_file_name is not None:
fileConfig(config.config_file_name)

logger = logging.getLogger("alembic.env")

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata


# cape_cod_db has its own config file and which contains database url.
# Additionally for migrations we're running from an ephemeral virtualenv that
# may not have a good .env file available.
# we're going to ignore what's set in the alembic ini then check the following
# in decreasing order of precedence:
# - an environment variable named "DB_URL"
# - the project .env file for a key of "DB_URL"
# We'll consider one of those to be required and will error out if not found.
# we have 3 ways to specify the database url (higher in list is higher
# precedence):
# - command line parameter `db_url`
# - env var `DB_URL`
# - alembic config file value `sqlalchemy.url`

db_url = os.getenv("DB_URL")
cli_args = context.get_x_argument(as_dictionary=True)

if db_url is None:
# TODO: we also specify a log level in that config. we *could* use that here,
# but for now we're going to respect what's in the alembic ini since the
# logging here is for a different purpose than the logging in the db app's
# setup
proj_config = dotenv_values(os.path.join(proj_root, ".env"))
db_url = proj_config.get("DB_URL")
# first try a cli arg
db_url = cli_args.get("db_url", None)

# then an env var
if db_url is None:
print(
"No DB_URL configured in environment variable or project level .env "
"file. Cannot continue."
)
# if we don't have this we have problems. so we are bailing
sys.exit(1)

db_url = os.getenv("DB_URL")

config.set_main_option("sqlalchemy.url", db_url)
# we've already got the file config, so if db_url is not None by here, overwrite
# the file config value
if db_url is not None:
config.set_main_option("sqlalchemy.url", db_url)

logging.info(
f"Configured for database: {config.get_main_option('sqlalchemy.url')}"
)

# other values from the config, defined by the needs of env.py,
# can be acquired:
Expand Down
File renamed without changes.
Loading