Skip to content

Commit 825062f

Browse files
Merge branch 'main' into AAI-127-email-approvers
2 parents 32e0f73 + 5c652f8 commit 825062f

19 files changed

Lines changed: 572 additions & 2 deletions

File tree

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ CORS_ALLOWED_ORIGINS=http://localhost:8000
1818
# AWS SES configs - required for running tests locally. Ask amanda@biocommons.org.au for credentials values
1919
AWS_ACCESS_KEY_ID=<aws-access-key-id>
2020
AWS_SECRET_ACCESS_KEY=<aws-secret-access-key>
21+
# Database config: we do this differently for local dev vs. on AWS
22+
# NOTE: DB_HOST is used first if present, so don't include it
23+
# if you want a local DB
24+
# Local dev: supply the full DB connection string as DB_URL
25+
DB_URL=sqlite:///mydatabase.db
26+
# AWS: supply DB_HOST, with the host name and port
27+
# DB_HOST=mydb.amazonaws.com:5432
28+

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
AWS_CERTIFICATE_ARN=${{ secrets.AWS_CERTIFICATE_ARN }}
5656
AWS_ZONE_ID=${{ secrets.AWS_ZONE_ID }}
5757
AWS_ZONE_DOMAIN=${{ secrets.AWS_ZONE_DOMAIN }}
58+
AWS_DB_HOST=${{ secrets.AWS_DB_HOST }}
5859
EOF
5960
6061
- name: CDK Deploy

alembic.ini

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
script_location = migrations
6+
7+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8+
# Uncomment the line below if you want the files to be prepended with date and time
9+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10+
# for all available tokens
11+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12+
13+
# sys.path path, will be prepended to sys.path if present.
14+
# defaults to the current working directory.
15+
prepend_sys_path = .
16+
17+
# timezone to use when rendering the date within the migration file
18+
# as well as the filename.
19+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
20+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
21+
# string value is passed to ZoneInfo()
22+
# leave blank for localtime
23+
# timezone =
24+
25+
# max length of characters to apply to the
26+
# "slug" field
27+
# truncate_slug_length = 40
28+
29+
# set to 'true' to run the environment during
30+
# the 'revision' command, regardless of autogenerate
31+
# revision_environment = false
32+
33+
# set to 'true' to allow .pyc and .pyo files without
34+
# a source .py file to be detected as revisions in the
35+
# versions/ directory
36+
# sourceless = false
37+
38+
# version location specification; This defaults
39+
# to migrations/versions. When using multiple version
40+
# directories, initial revisions must be specified with --version-path.
41+
# The path separator used here should be the separator specified by "version_path_separator" below.
42+
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
43+
44+
# version path separator; As mentioned above, this is the character used to split
45+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47+
# Valid values for version_path_separator are:
48+
#
49+
# version_path_separator = :
50+
# version_path_separator = ;
51+
# version_path_separator = space
52+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53+
54+
# set to 'true' to search source files recursively
55+
# in each "version_locations" directory
56+
# new in Alembic version 1.10
57+
# recursive_version_locations = false
58+
59+
# the output encoding used when revision files
60+
# are written from script.py.mako
61+
# output_encoding = utf-8
62+
63+
# NOTE: we set the sqlalchemy URL in migrations/env.py
64+
# as we need to set it dynamically based on environment
65+
#sqlalchemy.url = driver://user:pass@localhost/dbname
66+
67+
68+
[post_write_hooks]
69+
# post_write_hooks defines scripts or Python functions that are run
70+
# on newly generated revision scripts. See the documentation for further
71+
# detail and examples
72+
73+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
74+
# hooks = black
75+
# black.type = console_scripts
76+
# black.entrypoint = black
77+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
78+
79+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
80+
# hooks = ruff
81+
# ruff.type = exec
82+
# ruff.executable = %(here)s/.venv/bin/ruff
83+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
84+
85+
# Logging configuration
86+
[loggers]
87+
keys = root,sqlalchemy,alembic
88+
89+
[handlers]
90+
keys = console
91+
92+
[formatters]
93+
keys = generic
94+
95+
[logger_root]
96+
level = WARN
97+
handlers = console
98+
qualname =
99+
100+
[logger_sqlalchemy]
101+
level = WARN
102+
handlers =
103+
qualname = sqlalchemy.engine
104+
105+
[logger_alembic]
106+
level = INFO
107+
handlers =
108+
qualname = alembic
109+
110+
[handler_console]
111+
class = StreamHandler
112+
args = (sys.stderr,)
113+
level = NOTSET
114+
formatter = generic
115+
116+
[formatter_generic]
117+
format = %(levelname)-5.5s [%(name)s] %(message)s
118+
datefmt = %H:%M:%S

db/__init__.py

Whitespace-only changes.

db/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import uuid
2+
from datetime import datetime, timezone
3+
from typing import Literal
4+
5+
from pydantic import AwareDatetime
6+
from sqlmodel import AutoString, DateTime, Field, SQLModel
7+
8+
ApprovalStatus = Literal["approved", "pending", "revoked"]
9+
10+
11+
class GroupMembership(SQLModel, table=True):
12+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
13+
# TODO: May want to constrain the types of strings
14+
# given they have a specific format
15+
# TODO: May want to make group and/or user_id indexes?
16+
group: str
17+
user_id: str
18+
user_email: str
19+
approval_status: ApprovalStatus = Field(sa_type=AutoString)
20+
updated_at: AwareDatetime = Field(default_factory=lambda: datetime.now(timezone.utc), sa_type=DateTime)
21+
updated_by_id: str
22+
updated_by_email: str

db/setup.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
from typing import Tuple
3+
4+
from dotenv import dotenv_values
5+
from sqlmodel import Session, SQLModel, create_engine
6+
7+
8+
def get_db_config() -> Tuple[str, dict]:
9+
"""
10+
Get database configuration from environment variables
11+
or the .env file
12+
"""
13+
# Get database URL
14+
# Case 1: AWS: we need to assemble the DB url from different
15+
# environment variables (as these need to be populated from
16+
# secrets)
17+
host = os.getenv("DB_HOST", None)
18+
if host is not None:
19+
user = os.getenv("DB_USER")
20+
password = os.getenv("DB_PASSWORD")
21+
db_url = f"postgresql+psycopg://{user}:{password}@{host}"
22+
return db_url, {}
23+
# Case 2: we have DB_URL set in the .env file, or we just want
24+
# an in-memory DB for dev/testing
25+
# Doing this separately from pydantic-settings as we
26+
# need this before loading the FastAPI app
27+
env_values = dotenv_values(".env")
28+
# Prefer the explicitly set value in .env, then environment variable,
29+
# fallback to in-memory DB
30+
db_url = env_values.get("DB_URL") or os.getenv("DB_URL") or "sqlite://"
31+
if db_url.startswith("sqlite://"):
32+
connect_args = {"check_same_thread": False}
33+
else:
34+
connect_args = {}
35+
return db_url, connect_args
36+
37+
38+
DB_URL, db_connect_args = get_db_config()
39+
engine = create_engine(DB_URL, connect_args=db_connect_args)
40+
41+
42+
def create_db_and_tables():
43+
SQLModel.metadata.create_all(engine)
44+
45+
46+
def get_db_session():
47+
with Session(engine) as session:
48+
yield session

deploy/aai_backend_deploy/aai_backend_deploy_stack.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from aws_cdk import (
2626
aws_route53 as route53,
2727
)
28+
from aws_cdk import (
29+
aws_secretsmanager as secretsmanager,
30+
)
2831
from constructs import Construct
2932

3033

@@ -36,9 +39,14 @@ def __init__(self, scope: Construct, construct_id: str, config: dict, **kwargs)
3639
self.certificate_arn = config["AWS_CERTIFICATE_ARN"]
3740
self.zone_id = config["AWS_ZONE_ID"]
3841
self.zone_domain = config["AWS_ZONE_DOMAIN"]
42+
self.db_host = config["AWS_DB_HOST"]
3943
except KeyError as e:
4044
raise ValueError(f"Missing required configuration: {e}. These should be set in .env locally, or GitHub Secrets.")
4145

46+
db_secret = secretsmanager.Secret.from_secret_name_v2(
47+
self, "DbCredentials", "aai-backend-db-credentials"
48+
)
49+
4250
# VPC
4351
vpc = ec2.Vpc(self, "AaiBackendVPC", max_azs=2)
4452

@@ -64,6 +72,11 @@ def __init__(self, scope: Construct, construct_id: str, config: dict, **kwargs)
6472
environment={
6573
"FORCE_REDEPLOY": str(datetime.datetime.now())
6674
},
75+
secrets={
76+
"DB_USER": ecs.Secret.from_secrets_manager(db_secret, field="username"),
77+
"DB_PASSWORD": ecs.Secret.from_secrets_manager(db_secret, field="password"),
78+
"DB_HOST": self.db_host,
79+
},
6780
logging=ecs.LogDrivers.aws_logs(stream_prefix="FastAPI"),
6881
)
6982

deploy/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def get_dotenv_config():
1212
"AWS_CERTIFICATE_ARN": os.getenv("AWS_CERTIFICATE_ARN"),
1313
"AWS_ZONE_ID": os.getenv("AWS_ZONE_ID"),
1414
"AWS_ZONE_DOMAIN": os.getenv("AWS_ZONE_DOMAIN"),
15+
"AWS_DB_HOST": os.getenv("AWS_DB_HOST"),
1516
}
1617

1718
config = get_dotenv_config()

main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from contextlib import asynccontextmanager
12

23
from dotenv import dotenv_values
34
from fastapi import FastAPI
45
from starlette.middleware.cors import CORSMiddleware
56

7+
# This has to be imported even if unused
8+
from db import models # noqa: F401
9+
from db.setup import create_db_and_tables
610
from routers import admin, bpa_register, galaxy_register, user, utils
711

812
# Load .env to get CORS_ALLOWED_ORIGINS.
@@ -14,7 +18,13 @@
1418
origin.strip() for origin in env_values.get("CORS_ALLOWED_ORIGINS", "").split(",")
1519
]
1620

17-
app = FastAPI()
21+
22+
@asynccontextmanager
23+
async def lifespan(app: FastAPI):
24+
create_db_and_tables()
25+
yield
26+
27+
app = FastAPI(lifespan=lifespan)
1828
app.add_middleware(
1929
CORSMiddleware,
2030
allow_origins=ALLOWED_ORIGINS,

migrations/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Generic single-database configuration.

0 commit comments

Comments
 (0)