Skip to content

Commit

Permalink
✨ Add issuer/verifier image_url to trustregistry (#983)
Browse files Browse the repository at this point in the history
* add alembic to TR

* run alembic init

* create initial migration

* 🎨

* alembic creates the tables now

* # pylint: disable=redefined-outer-name

* 🎨

* stamp the db

* 🎨

* poetry lock

* update test for new alembic code

* remove unused

* 🎨

* re-add mock_engine to tests

* # pylint: disable=unused-argument

* 🎨

* poetry lock

* remove url from logs

* fix db stamping logic

* 🎨

* update tests for main

* # pylint: disable=W0718

* 🎨

* stamp the db

* 🎨

* add image_url to register_actor call

* add image_url to update tenant logic

* update/fix tests

* add image_url to actor model

* add image_url to update actor crud

* add image_url to db actor model

* 🎨

* test new url is on TR with tenant update

* 🎨

* 🎨

* add alembic to trustregistry

* generate migration

* expose trustregistry db to localhost

* 🎨

* 🎨 import sort

* 🎨

* regenerate migration script for image_url

* 🎨

* poetry lock

* clean up logging

* # pylint: disable=redefined-outer-name

* remove unused import

* 🎨

* fix migration logic for empty db

* update tests

* 🎨

* update readme

* 🎨

* rename engine in check_migrations

* remove alembic from dev-dependencies

* 🎨
  • Loading branch information
cl0ete authored Oct 14, 2024
1 parent e02867d commit 5b0f052
Show file tree
Hide file tree
Showing 19 changed files with 806 additions and 89 deletions.
1 change: 1 addition & 0 deletions app/routes/admin/tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ async def create_tenant(
roles=roles,
did=onboard_result.did,
didcomm_invitation=onboard_result.didcomm_invitation,
image_url=body.image_url,
)
)
except HTTPException as http_error:
Expand Down
7 changes: 6 additions & 1 deletion app/services/onboarding/tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async def handle_tenant_update(

new_roles = update_request.roles or []
new_label = update_request.wallet_label
new_image_url = update_request.image_url

# See if this wallet belongs to an actor
actor = await fetch_actor_by_id(wallet_id)
Expand All @@ -48,9 +49,10 @@ async def handle_tenant_update(

if actor:
existing_roles = actor.roles
existing_image_url = actor.image_url
added_roles = list(set(new_roles) - set(existing_roles))

if new_label or added_roles: # Only update actor if
if new_label or added_roles or new_image_url: # Only update actor if
update_dict = {}
if new_label:
update_dict["name"] = new_label
Expand All @@ -76,6 +78,9 @@ async def handle_tenant_update(
update_dict["did"] = onboard_result.did
update_dict["didcomm_invitation"] = onboard_result.didcomm_invitation

if new_image_url and new_image_url != existing_image_url:
update_dict["image_url"] = new_image_url

updated_actor = actor.model_copy(update=update_dict)

await update_actor(updated_actor)
Expand Down
1 change: 1 addition & 0 deletions app/tests/e2e/test_tenants.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ async def test_update_tenant_verifier_to_issuer(
assert_that(new_actor).has_name(new_wallet_label)
assert_that(new_actor).has_did(new_actor.did)
assert_that(new_actor.roles).contains_only("issuer", "verifier")
assert_that(new_actor.image_url).is_equal_to(new_image_url)

assert new_actor.didcomm_invitation is not None
finally:
Expand Down
1 change: 1 addition & 0 deletions app/tests/routes/admin_tenants/test_create_tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ async def test_create_tenant_success(roles):
roles=roles,
did=did,
didcomm_invitation=didcomm_invitation,
image_url=create_tenant_body.image_url,
)
)

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ services:
- POSTGRES_USER=trustregistry
- POSTGRES_PASSWORD=trustregistry
- PGUSER=trustregistry
ports:
- 0.0.0.0:5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready", "-d", "${POSTGRES_DB}"]
interval: 10s
Expand Down
1 change: 1 addition & 0 deletions shared/models/trustregistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Actor(BaseModel):
roles: List[TrustRegistryRole]
did: str
didcomm_invitation: Optional[str] = None
image_url: Optional[str] = None

@field_validator("did")
@classmethod
Expand Down
117 changes: 117 additions & 0 deletions trustregistry/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = 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
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = ../

# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions

# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = postgresql://trustregistry:[email protected]:5432/trustregistry


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME

# 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
1 change: 1 addition & 0 deletions trustregistry/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def update_actor(db_session: Session, actor: Actor) -> db.Actor:
roles=actor.roles,
didcomm_invitation=actor.didcomm_invitation,
did=actor.did,
image_url=actor.image_url if actor.image_url else db_actor.image_url,
)
.returning(db.Actor)
)
Expand Down
1 change: 1 addition & 0 deletions trustregistry/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Actor(Base):
String, unique=True, index=True
)
did: Mapped[str] = mapped_column(String, unique=True, index=True)
image_url: Mapped[Optional[str]] = mapped_column(String, index=True)


class Schema(Base):
Expand Down
58 changes: 55 additions & 3 deletions trustregistry/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import os
from contextlib import asynccontextmanager

from alembic import command
from alembic.config import Config
from alembic.migration import MigrationContext
from alembic.script import ScriptDirectory
from fastapi import Depends, FastAPI
from scalar_fastapi import get_scalar_api_reference
from sqlalchemy import inspect
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session

from shared.constants import PROJECT_VERSION
from shared.log_config import get_logger
from trustregistry import crud, db
from trustregistry import crud
from trustregistry.database import engine
from trustregistry.db import get_db
from trustregistry.registry import registry_actors, registry_schemas
Expand All @@ -19,10 +24,57 @@
ROOT_PATH = os.getenv("ROOT_PATH", "")


def check_migrations(db_engine: Engine, alembic_cfg: Config) -> bool:
# Check if alembic_version table exists
with db_engine.connect() as connection:
inspector = inspect(connection)
table_names = inspector.get_table_names()
has_alembic_version = "alembic_version" in table_names
has_actors_table = "actors" in table_names

script = ScriptDirectory.from_config(alembic_cfg)
if not has_alembic_version and has_actors_table:
logger.info(
"Alembic version table not found. Stamping with initial revision..."
)
try:
initial_revision = script.get_base()
command.stamp(alembic_cfg, initial_revision)
logger.info(
"Database stamped with initial migration version: {}", initial_revision
)
except Exception: # pylint: disable=W0718
logger.exception("Error stamping database")
raise

elif not has_alembic_version:
logger.info("Alembic version table not found.")
return False

# Get current revision
with db_engine.connect() as connection:
context = MigrationContext.configure(connection)
current_rev = context.get_current_revision()

head_rev = script.get_current_head()

return current_rev == head_rev


@asynccontextmanager
async def lifespan(_: FastAPI):
db.Base.metadata.create_all(bind=engine)
engine.dispose()
alembic_cfg = Config("alembic.ini")

if not check_migrations(engine, alembic_cfg):
logger.info("Applying database migrations...")
try:
command.upgrade(alembic_cfg, "head")
logger.info("Database schema is up to date.")
except Exception: # pylint: disable=broad-except
logger.exception("Error during migration")
raise
else:
logger.info("Database is up to date. No migrations needed.")

logger.debug("TrustRegistry startup: Validate tables are created")
with engine.connect() as connection:
Expand Down
15 changes: 15 additions & 0 deletions trustregistry/migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Alembic Migrations

## Overview

This folder contains the migration scripts for the trust registry database.
These scripts are used to manage changes to the database schema in a consistent and version-controlled manner.

### Files and Directories

- `env.py`: This is the configuration file for Alembic, the database migration tool.
It sets up the database connection and other settings required for migrations.
- `script.py.mako`: This is a template file used by Alembic to generate new migration scripts.
It contains placeholders for the migration logic.
- `versions/`: This directory contains the individual migration scripts.
Each script is named with a unique identifier and describes a specific change to the database schema.
Loading

0 comments on commit 5b0f052

Please sign in to comment.