generated from opensafely-core/repo-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from ebmdatalab/sqlalchemy
Switch to SQLAlchemy from raw strings
- Loading branch information
Showing
11 changed files
with
211 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,23 @@ | ||
github_pull_requests = """ | ||
CREATE TABLE IF NOT EXISTS github_pull_requests ( | ||
time TIMESTAMP WITH TIME ZONE NOT NULL, | ||
name TEXT NOT NULL, | ||
value INTEGER NOT NULL, | ||
author TEXT NOT NULL, | ||
organisation TEXT NOT NULL, | ||
repo TEXT NOT NULL, | ||
CONSTRAINT github_pull_requests_must_be_different UNIQUE (time, name, author, repo) | ||
); | ||
""" | ||
slack_tech_support = """ | ||
CREATE TABLE IF NOT EXISTS slack_tech_support ( | ||
time TIMESTAMP WITH TIME ZONE NOT NULL, | ||
name TEXT NOT NULL, | ||
value INTEGER NOT NULL, | ||
CONSTRAINT slack_tech_support_must_be_different UNIQUE (time, name) | ||
); | ||
""" | ||
from sqlalchemy import TIMESTAMP, Column, Integer, MetaData, Table, Text | ||
|
||
|
||
metadata = MetaData() | ||
|
||
GitHubPullRequests = Table( | ||
"github_pull_requests", | ||
metadata, | ||
Column("time", TIMESTAMP(timezone=True), primary_key=True), | ||
Column("name", Text, primary_key=True), | ||
Column("value", Integer), | ||
Column("author", Text, primary_key=True), | ||
Column("organisation", Text), | ||
Column("repo", Text, primary_key=True), | ||
) | ||
|
||
SlackTechSupport = Table( | ||
"slack_tech_support", | ||
metadata, | ||
Column("time", TIMESTAMP(timezone=True), primary_key=True), | ||
Column("name", Text, primary_key=True), | ||
Column("value", Integer), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,79 +1,75 @@ | ||
import os | ||
from datetime import datetime, time | ||
|
||
import psycopg | ||
import structlog | ||
|
||
from . import tables | ||
from sqlalchemy import create_engine, inspect, schema, text | ||
from sqlalchemy.dialects.postgresql import insert | ||
|
||
|
||
log = structlog.get_logger() | ||
|
||
TIMESCALEDB_URL = os.environ["TIMESCALEDB_URL"] | ||
# Note: psycopg2 is still the default postgres dialect for sqlalchemy so we | ||
# inject +psycopg to enable using v3 | ||
TIMESCALEDB_URL = os.environ["TIMESCALEDB_URL"].replace( | ||
"postgresql", "postgresql+psycopg" | ||
) | ||
|
||
|
||
def ensure_table(name): | ||
def ensure_table(engine, table): | ||
""" | ||
Ensure both the table and hypertable config exist in the database | ||
""" | ||
run(getattr(tables, name)) | ||
|
||
run( | ||
"SELECT create_hypertable(%s, 'time', if_not_exists => TRUE);", | ||
[name], | ||
) | ||
|
||
# ensure the RO grafana user can read the table | ||
run(f"GRANT SELECT ON {name} TO grafanareader") | ||
|
||
|
||
def run(sql, *args): | ||
with psycopg.connect(TIMESCALEDB_URL) as conn: | ||
cursor = conn.cursor() | ||
with engine.begin() as connection: | ||
connection.execute(schema.CreateTable(table, if_not_exists=True)) | ||
|
||
with engine.begin() as connection: | ||
connection.execute( | ||
text( | ||
f"SELECT create_hypertable('{table.name}', 'time', if_not_exists => TRUE);" | ||
) | ||
) | ||
|
||
return cursor.execute(sql, *args) | ||
# ensure the RO grafana user can read the table | ||
connection.execute(text(f"GRANT SELECT ON {table.name} TO grafanareader")) | ||
|
||
|
||
class TimescaleDBWriter: | ||
def __init__(self, table, key): | ||
self.key = key | ||
inserts = [] | ||
|
||
def __init__(self, table): | ||
self.table = table | ||
self.engine = create_engine(TIMESCALEDB_URL) | ||
|
||
def __enter__(self): | ||
ensure_table(self.table) | ||
ensure_table(self.engine, self.table) | ||
|
||
return self | ||
|
||
def __exit__(self, *args): | ||
pass | ||
with self.engine.begin() as connection: | ||
for stmt in self.inserts: | ||
connection.execute(stmt) | ||
|
||
def write(self, date, value, **kwargs): | ||
# convert date to a timestamp | ||
# TODO: do we need to do any checking to make sure this is tz-aware and in | ||
# UTC? | ||
dt = datetime.combine(date, time()) | ||
|
||
# insert into the table set at instantiation | ||
# unique by the tables `{name}_must_be_different` and we always want to | ||
# bump the value if that triggers a conflict | ||
# the columns could differ per table… do we want an object to represent tables? | ||
if kwargs: | ||
extra_fields = ", " + ", ".join(kwargs.keys()) | ||
placeholders = ", " + ", ".join(["%s" for k in kwargs.keys()]) | ||
else: | ||
extra_fields = "" | ||
placeholders = "" | ||
sql = f""" | ||
INSERT INTO {self.table} (time, name, value {extra_fields}) | ||
VALUES (%s, %s, %s {placeholders}) | ||
ON CONFLICT ON CONSTRAINT {self.table}_must_be_different DO UPDATE SET value = EXCLUDED.value; | ||
""" | ||
|
||
run(sql, (dt, self.key, value, *kwargs.values())) | ||
|
||
log.debug( | ||
self.key, | ||
date=dt.isoformat(), | ||
value=value, | ||
**kwargs, | ||
# get the primary key name from the given table | ||
constraint = inspect(self.engine).get_pk_constraint(self.table.name)["name"] | ||
|
||
# TODO: could we put do all the rows at once in the values() call and | ||
# then use EXCLUDED to reference the value in the set_? | ||
insert_stmt = ( | ||
insert(self.table) | ||
.values(time=dt, value=value, **kwargs) | ||
.on_conflict_do_update( | ||
constraint=constraint, | ||
set_={"value": value}, | ||
) | ||
) | ||
|
||
self.inserts.append(insert_stmt) | ||
|
||
log.debug(insert_stmt) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.