Skip to content

Commit

Permalink
api: implement /healthz (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
JJMC89 committed Apr 3, 2024
1 parent 8e5e44c commit e48f53e
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 14 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ license = "MIT"
name = "copypatrol-backend"
readme = "README.md"
repository = "https://github.com/JJMC89/copypatrol-backend"
version = "2024.2.19"
version = "2024.4.3"

[tool.poetry.dependencies]
python = ">=3.11,<4"
Expand Down
58 changes: 55 additions & 3 deletions src/copypatrol_backend/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,61 @@ def application_factory() -> flask.Flask:
def empty() -> ResponseReturnValue:
return {}

@app.route("/heartbeat")
def heartbeat() -> ResponseReturnValue:
return {"status": "healthy"}
@app.route("/healthz")
def healthz() -> ResponseReturnValue:
with database.create_sessionmaker().begin() as session:
queue_length = database.diff_count(
session,
database.QueuedDiff,
)
if queue_length > 0:
queue: tca.JSON = {
"length": queue_length,
"newest": database.max_diff_timestamp(
session,
database.QueuedDiff,
).isoformat(),
"oldest": database.min_diff_timestamp(
session,
database.QueuedDiff,
).isoformat(),
}
else:
queue = {
"length": queue_length,
"newest": None,
"oldest": None,
}
ready_length = database.diff_count(
session,
database.Diff,
status=database.Status.READY,
)
if ready_length > 0:
ready: tca.JSON = {
"length": ready_length,
"newest": database.max_diff_timestamp(
session,
database.Diff,
status=database.Status.READY,
).isoformat(),
"oldest": database.min_diff_timestamp(
session,
database.Diff,
status=database.Status.READY,
).isoformat(),
}
else:
ready = {
"length": ready_length,
"newest": None,
"oldest": None,
}
return {
"queue": queue,
"ready": ready,
"status": "up",
}

@app.route("/tca-webhook", methods=["POST"])
@requires_tca_webhook_headers
Expand Down
48 changes: 48 additions & 0 deletions src/copypatrol_backend/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TypeDecorator,
and_,
create_engine,
func,
select,
)
from sqlalchemy.orm import (
Expand Down Expand Up @@ -413,3 +414,50 @@ def queued_diffs_by_status(
.limit(limit)
)
return session.scalars(stmt).all()


def diff_count(
session: Session,
table_class: type[DiffMixin],
/,
*,
status: Status | list[Status] | None = None,
) -> int:
stmt = select(func.count()).select_from(table_class)
if status is not None:
if isinstance(status, Status):
status = [status]
stmt = stmt.where(table_class.status.in_(status))
res = session.scalar(stmt)
assert res is not None
return res


def max_diff_timestamp(
session: Session,
table_class: type[DiffMixin],
/,
*,
status: Status | list[Status] | None = None,
) -> Timestamp:
stmt = select(func.max(table_class.status_timestamp))
if status is not None:
if isinstance(status, Status):
status = [status]
stmt = stmt.where(table_class.status.in_(status))
return session.scalar(stmt)


def min_diff_timestamp(
session: Session,
table_class: type[DiffMixin],
/,
*,
status: Status | list[Status] | None = None,
) -> Timestamp:
stmt = select(func.min(table_class.status_timestamp))
if status is not None:
if isinstance(status, Status):
status = [status]
stmt = stmt.where(table_class.status.in_(status))
return session.scalar(stmt)
190 changes: 180 additions & 10 deletions tests/integration/database_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,24 @@

@pytest.fixture
def diffs_data(request, db_session):
sql_dct = {
k: v.encode() if isinstance(v, str) else v
for k, v in request.param.items()
}
cols = ", ".join(sql_dct)
vals = ", ".join(f":{key}" for key in sql_dct)
stmt = text(f"INSERT INTO `diffs_queue` ({cols}) VALUES ({vals})")
db_session.execute(stmt, sql_dct)
if isinstance(request.param, tuple):
dcts = request.param
else:
dcts = (request.param,)
for dct in dcts:
sql_dct = {
k: v.encode() if isinstance(v, str) else v for k, v in dct.items()
}
cols = ", ".join(sql_dct)
vals = ", ".join(f":{key}" for key in sql_dct)
stmt = text(f"INSERT INTO `diffs_queue` ({cols}) VALUES ({vals})")
db_session.execute(stmt, sql_dct)
db_session.commit()
yield


def test_diff_from_queueddiff():
rev_timestamp = pywikibot.Timestamp.utcnow()
rev_timestamp = Timestamp.utcnow()
diff = database.QueuedDiff(
project="wikipedia",
lang="es",
Expand Down Expand Up @@ -81,7 +85,7 @@ def test_diff_properties():
page_title="Examplé",
rev_id=1000,
rev_parent_id=1001,
rev_timestamp=pywikibot.Timestamp.utcnow(),
rev_timestamp=Timestamp.utcnow(),
rev_user_text="Example",
)
site = pywikibot.Site("es", "wikipedia")
Expand Down Expand Up @@ -181,3 +185,169 @@ def test_queued_diffs_by_status(db_session, diffs_data, status):
delta=datetime.timedelta(hours=-1),
)
assert len(result) == 0


@pytest.mark.parametrize(
"diffs_data",
[
tuple(
{
"project": "wikipedia",
"lang": "en",
"page_namespace": 0,
"page_title": "Diff_count",
"rev_id": 4000 + status,
"rev_parent_id": 4000,
"rev_timestamp": "20220101010101",
"rev_user_text": "Example",
"status": status,
"status_timestamp": Timestamp.utcnow().totimestampformat(),
}
for status in range(-4, 0)
)
],
indirect=["diffs_data"],
)
def test_diff_count(db_session, diffs_data):
assert database.diff_count(db_session, database.QueuedDiff) == 4
for status in range(-4, 0):
assert (
database.diff_count(
db_session,
database.QueuedDiff,
status=database.Status(status),
)
== 1
)
assert (
database.diff_count(
db_session,
database.QueuedDiff,
status=[database.Status(status), database.Status.READY],
)
== 1
)


@pytest.mark.parametrize(
"table_class",
[database.Diff, database.QueuedDiff],
)
def test_diff_count_empty(db_session, table_class):
assert database.diff_count(db_session, table_class) == 0


@pytest.mark.parametrize(
"diffs_data",
[
(
{
"project": "wikipedia",
"lang": "en",
"page_namespace": 0,
"page_title": "Max_min_diff_timestamp",
"rev_id": 5001,
"rev_parent_id": 5000,
"rev_timestamp": "20220101010101",
"rev_user_text": "Example",
"status": -1,
"status_timestamp": "20220101010101",
},
{
"project": "wikipedia",
"lang": "en",
"page_namespace": 0,
"page_title": "Max_min_diff_timestamp",
"rev_id": 5002,
"rev_parent_id": 5000,
"rev_timestamp": "20220101010101",
"rev_user_text": "Example",
"status": -2,
"status_timestamp": "20220101010202",
},
{
"project": "wikipedia",
"lang": "en",
"page_namespace": 0,
"page_title": "Max_min_diff_timestamp",
"rev_id": 5003,
"rev_parent_id": 5000,
"rev_timestamp": "20220101010101",
"rev_user_text": "Example",
"status": -3,
"status_timestamp": "20220101010303",
},
)
],
indirect=True,
)
def test_max_min_diff_timestamp(db_session, diffs_data):
_1 = Timestamp.fromisoformat("2022-01-01T01:01:01")
_2 = Timestamp.fromisoformat("2022-01-01T01:02:02")
_3 = Timestamp.fromisoformat("2022-01-01T01:03:03")
assert database.max_diff_timestamp(db_session, database.QueuedDiff) == _3
assert (
database.max_diff_timestamp(
db_session,
database.QueuedDiff,
status=database.Status.PENDING,
)
== _1
)
assert (
database.max_diff_timestamp(
db_session,
database.QueuedDiff,
status=database.Status.UPLOADED,
)
== _2
)
assert (
database.max_diff_timestamp(
db_session,
database.QueuedDiff,
status=database.Status.CREATED,
)
== _3
)
assert (
database.max_diff_timestamp(
db_session,
database.QueuedDiff,
status=[database.Status.PENDING, database.Status.UPLOADED],
)
== _2
)
assert database.min_diff_timestamp(db_session, database.QueuedDiff) == _1
assert (
database.min_diff_timestamp(
db_session,
database.QueuedDiff,
status=database.Status.PENDING,
)
== _1
)
assert (
database.min_diff_timestamp(
db_session,
database.QueuedDiff,
status=database.Status.UPLOADED,
)
== _2
)
assert (
database.min_diff_timestamp(
db_session,
database.QueuedDiff,
status=database.Status.CREATED,
)
== _3
)
assert (
database.min_diff_timestamp(
db_session,
database.QueuedDiff,
status=[database.Status.PENDING, database.Status.UPLOADED],
)
== _1
)

0 comments on commit e48f53e

Please sign in to comment.