Skip to content

Commit

Permalink
Update dev tools (#52)
Browse files Browse the repository at this point in the history
Use ruff instead of pylint and isort.
Also, add pyupgrade.

Configure ruff starting from what is suggested at
https://learn.scientific-python.org/development/guides/style/

Update pre-commit hooks and dependency versions.

Update code base according to new dev tools.

Use `typing.Annotated` for all pydantic and FastAPI stuff.

Use lifespan in FastAPI.
  • Loading branch information
CasperWA authored Nov 22, 2023
1 parent d3080a6 commit 8cac361
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 281 deletions.
53 changes: 37 additions & 16 deletions .github/utils/docker_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Run tests for the service."""
# pylint: disable=import-error
from __future__ import annotations

import argparse
import json
import os
Expand All @@ -10,13 +11,14 @@

import requests
from dlite import Instance
from fastapi import status
from pymongo import MongoClient

if TYPE_CHECKING:
from typing import Any, Literal


DLITE_TEST_ENTITIES: "list[dict[str, Any]]" = [
DLITE_TEST_ENTITIES: list[dict[str, Any]] = [
{
"uri": "http://onto-ns.com/meta/0.1/Person",
"meta": "http://onto-ns.com/meta/0.3/EntitySchema",
Expand Down Expand Up @@ -104,14 +106,15 @@

def add_testdata() -> None:
"""Add MongoDB test data."""
mongodb_user = os.getenv("entity_service_mongo_user")
mongodb_pass = os.getenv("entity_service_mongo_password")
mongodb_uri = os.getenv("entity_service_mongo_uri")
mongodb_user = os.getenv("ENTITY_SERVICE_MONGO_USER")
mongodb_pass = os.getenv("ENTITY_SERVICE_MONGO_PASSWORD")
mongodb_uri = os.getenv("ENTITY_SERVICE_MONGO_URI")
if any(_ is None for _ in (mongodb_user, mongodb_pass, mongodb_uri)):
raise ValueError(
"'entity_service_mongo_uri', 'entity_service_mongo_user', and "
"'entity_service_mongo_password' environment variables MUST be specified."
error_message = (
"ENTITY_SERVICE_MONGO_URI, ENTITY_SERVICE_MONGO_USER, and "
"ENTITY_SERVICE_MONGO_PASSWORD environment variables MUST be specified."
)
raise ValueError(error_message)

client = MongoClient(mongodb_uri, username=mongodb_user, password=mongodb_pass)
collection = client.dlite.entities
Expand All @@ -124,34 +127,46 @@ def _get_version_name(uri: str) -> tuple[str, str]:
r"^http://onto-ns\.com/meta/(?P<version>[^/]+)/(?P<name>[^/]+)$", uri
)
if match is None:
raise RuntimeError("Could not retrieve version and name from test entities.")
error_message = (
f"Could not retrieve version and name from {uri!r}. "
"URI must be of the form: "
"http://onto-ns.com/meta/{version}/{name}"
)
raise RuntimeError(error_message)
return match.group("version") or "", match.group("name") or ""


def _get_uri(entity: "dict[str, Any]") -> str:
def _get_uri(entity: dict[str, Any]) -> str:
"""Return the uri for an entity."""
namespace = entity.get("namespace")
version = entity.get("version")
name = entity.get("name")
if any(_ is None for _ in (namespace, version, name)):
raise RuntimeError(
error_message = (
"Could not retrieve namespace, version, and/or name from test entities."
)
raise RuntimeError(error_message)
return f"{namespace}/{version}/{name}"


def run_tests() -> None:
"""Test the service."""
host = os.getenv("DOCKER_TEST_HOST", "localhost")
port = os.getenv("DOCKER_TEST_PORT", "8000")

for test_entity in DLITE_TEST_ENTITIES:
uri = test_entity.get("uri")

if uri is None:
uri = _get_uri(test_entity)

if not isinstance(uri, str):
raise TypeError("uri must be a string")
error_message = f"uri must be a string. Got {type(uri)}."
raise TypeError(error_message)

version, name = _get_version_name(uri)
response = requests.get(f"http://{host}:{port}/{version}/{name}", timeout=5)

assert response.ok, (
f"Test data {uri!r} not found! (Or some other error).\n"
f"Response:\n{json.dumps(response.json(), indent=2)}"
Expand All @@ -161,18 +176,23 @@ def run_tests() -> None:
assert entity == test_entity
Instance.from_dict(test_entity)

# Test that the service returns a Not Found (404) for non existant URIs
version, name = _get_version_name("http://onto-ns.com/meta/0.3/EntitySchema")
response = requests.get(f"http://{host}:{port}/{version}/{name}", timeout=5)

assert not response.ok, "Non existant (valid) URI returned an OK response!"
assert (
response.status_code == 404
response.status_code == status.HTTP_404_NOT_FOUND
), f"Response:\n\n{json.dumps(response.json(), indent=2)}"

# Test that the service raises a pydantic ValidationError and returns an "
# "Unprocessable Entity (422) for invalid URIs
version, name = _get_version_name("http://onto-ns.com/meta/Entity/1.0")
response = requests.get(f"http://{host}:{port}/{version}/{name}", timeout=5)

assert not response.ok, "Invalid URI returned an OK response!"
assert (
response.status_code != 404
response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
), f"Response:\n\n{json.dumps(response.json(), indent=2)}"


Expand All @@ -189,14 +209,15 @@ def main(args: list[str] | None = None) -> None:
default="run_tests",
)

job: 'Literal["add-testdata","run-tests"]' = parser.parse_args(args).job
job: Literal["add-testdata", "run-tests"] = parser.parse_args(args).job

if job == "add-testdata":
add_testdata()
elif job == "run-tests":
run_tests()
else:
raise ValueError(f"{job!r} isn't a valid input.")
error_message = f"Invalid job {job!r}."
raise ValueError(error_message)


if __name__ == "__main__":
Expand Down
16 changes: 7 additions & 9 deletions .github/workflows/ci_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ jobs:

# pre-commit
python_version_pre-commit: "3.10"
skip_pre-commit_hooks: pylint

# pylint & safety
python_version_pylint_safety: "3.10"
pylint_runs: |
--rcfile=pyproject.toml --extension-pkg-whitelist='pydantic' dlite_entities_service
run_pylint: false

# Build dist
python_version_package: "3.10"
Expand Down Expand Up @@ -51,9 +49,9 @@ jobs:
MONGO_INITDB_ROOT_PASSWORD: root

env:
entity_service_mongo_uri: mongodb://localhost:27017
entity_service_mongo_user: root
entity_service_mongo_password: root
ENTITY_SERVICE_MONGO_URI: mongodb://localhost:27017
ENTITY_SERVICE_MONGO_USER: root
ENTITY_SERVICE_MONGO_PASSWORD: root
DOCKER_TEST_PORT: 8000

steps:
Expand All @@ -71,9 +69,9 @@ jobs:
- name: Run Docker container
run: |
docker run --rm -d \
--env entity_service_mongo_uri \
--env entity_service_mongo_user \
--env entity_service_mongo_password \
--env ENTITY_SERVICE_MONGO_URI \
--env ENTITY_SERVICE_MONGO_USER \
--env ENTITY_SERVICE_MONGO_PASSWORD \
--env PORT=${DOCKER_TEST_PORT} \
--name "entity-service" \
--network "host" \
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/ci_update_dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,5 @@ jobs:
update_pre-commit: true
python_version: "3.10"
install_extras: "[dev]"
skip_pre-commit_hooks: "pylint"
secrets:
PAT: ${{ secrets.TEAM40_PAT }}
61 changes: 20 additions & 41 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,31 @@ repos:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]

# isort is a tool to sort and group import statements in Python files
# It works on files in-place
- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black", "--filter-files", "--skip-gitignore"]

# Black is a code style and formatter
# It works on files in-place
- repo: https://github.com/ambv/black
rev: 23.10.1
rev: 23.11.0
hooks:
- id: black

# Ruff is a code style and formatter
# It works on files in-place
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args:
- --fix
- --show-fixes

# Pyupgrade is a code upgrade tool
# It works on files in-place
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: [--py310-plus]

# Bandit is a security linter
# More information can be found in its documentation:
# https://bandit.readthedocs.io/en/latest/
Expand All @@ -49,41 +59,10 @@ repos:
# The project's documentation can be found at:
# https://mypy.readthedocs.io/en/stable/index.html
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
rev: v1.7.0
hooks:
- id: mypy
exclude: ^docs/example/.*$$
additional_dependencies:
- pydantic>=2
- types-requests

- repo: local
hooks:
# pylint is a Python linter
# It is run through the local environment to ensure external packages can be
# imported without issue.
# For more information about pylint see its documentation at:
# https://pylint.pycqa.org/en/latest/
- id: pylint
name: pylint
entry: pylint
args: ["--rcfile=pyproject.toml"]
language: python
types: [python]
require_serial: true
exclude: ^(tests|docs/example)/.*$
# pylint is a Python linter
# It is run through the local environment to ensure external packages can be
# imported without issue.
# For more information about pylint see its documentation at:
# https://pylint.pycqa.org/en/latest/
# - id: pylint-tests
# name: pylint - tests
# entry: pylint
# args:
# - "--rcfile=pyproject.toml"
# - "--disable=import-outside-toplevel,redefined-outer-name"
# language: python
# types: [python]
# require_serial: true
# files: ^tests/.*$
1 change: 1 addition & 0 deletions dlite_entities_service/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""DLite entities service."""
from __future__ import annotations

__version__ = "0.0.1"
__author__ = "Casper Welzel Andersen"
2 changes: 2 additions & 0 deletions dlite_entities_service/backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Backend implementation."""
from __future__ import annotations

from pymongo import MongoClient

from dlite_entities_service.config import CONFIG
Expand Down
37 changes: 21 additions & 16 deletions dlite_entities_service/config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Service app configuration."""
from typing import Any
from __future__ import annotations

from typing import Annotated, Any

from pydantic import Field, SecretStr, field_validator
from pydantic.networks import AnyHttpUrl, MultiHostUrl, UrlConstraints
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing_extensions import Annotated

MongoDsn = Annotated[
MultiHostUrl, UrlConstraints(allowed_schemes=["mongodb", "mongodb+srv"])
Expand All @@ -15,20 +16,24 @@
class ServiceSettings(BaseSettings):
"""Service app configuration."""

base_url: AnyHttpUrl = Field(
AnyHttpUrl("http://onto-ns.com/meta"),
description="Base URL, where the service is running.",
)
mongo_uri: MongoDsn = Field(
MongoDsn("mongodb://localhost:27017"),
description="URI for the MongoDB cluster/server.",
)
mongo_user: str | None = Field(
None, description="Username for connecting to the MongoDB."
)
mongo_password: SecretStr | None = Field(
None, description="Password for connecting to the MongoDB."
)
base_url: Annotated[
AnyHttpUrl,
Field(
description="Base URL, where the service is running.",
),
] = AnyHttpUrl("http://onto-ns.com/meta")
mongo_uri: Annotated[
MongoDsn,
Field(
description="URI for the MongoDB cluster/server.",
),
] = MongoDsn("mongodb://localhost:27017")
mongo_user: Annotated[
str | None, Field(description="Username for connecting to the MongoDB.")
] = None
mongo_password: Annotated[
SecretStr | None, Field(description="Password for connecting to the MongoDB.")
] = None

@field_validator("base_url", mode="before")
@classmethod
Expand Down
3 changes: 2 additions & 1 deletion dlite_entities_service/logger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Logging to file."""
from __future__ import annotations

import logging
import sys
from contextlib import contextmanager
Expand All @@ -8,7 +10,6 @@
from uvicorn.logging import DefaultFormatter

if TYPE_CHECKING: # pragma: no cover
# pylint: disable=ungrouped-imports
import logging.handlers


Expand Down
Loading

0 comments on commit 8cac361

Please sign in to comment.