Skip to content

Commit dafd31f

Browse files
committed
Merge branch 'main' into fix-isort-errors
2 parents 7a2b3fe + 5e04aa0 commit dafd31f

File tree

12 files changed

+216
-124
lines changed

12 files changed

+216
-124
lines changed

python/hooks/post_gen_project.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515

1616
if not {{ cookiecutter.add_fastapi }}:
1717
Path("tests/test_api.py").unlink()
18+
Path("tests/data/service_info_openapi.yaml").unlink()
1819
Path("src/{{ cookiecutter.project_slug }}/api.py").unlink()
1920
Path("src/{{ cookiecutter.project_slug }}/models.py").unlink()
20-
Path("src/{{ cookiecutter.project_slug }}/config.py").unlink()
2121

2222
if (not {{ cookiecutter.add_fastapi }}) and (not {{ cookiecutter.add_cli }}):
2323
Path("./src/{{ cookiecutter.project_slug }}/logging.py").unlink()
24+
Path("src/{{ cookiecutter.project_slug }}/config.py").unlink()

python/{{cookiecutter.project_slug}}/.github/workflows/checks.yaml

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ jobs:
1515
with:
1616
python-version: {{ "${{ matrix.python-version }}" }}
1717

18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v6
20+
with:
21+
enable-cache: true
22+
1823
- name: Install dependencies
1924
run: |
20-
python3 -m pip install ".[tests]"
25+
run: uv sync --extra tests
2126

2227
- name: Run tests
23-
run: python3 -m pytest
28+
run: uv run pytest
2429

2530
lint:
2631
name: lint
@@ -33,11 +38,16 @@ jobs:
3338
with:
3439
python-version: "3.11"
3540

41+
- name: Install uv
42+
uses: astral-sh/setup-uv@v6
43+
with:
44+
enable-cache: true
45+
3646
- name: Install dependencies
37-
run: python3 -m pip install ".[dev]"
47+
run: uv sync --extra dev
3848

3949
- name: Check style
40-
run: python3 -m ruff check . && python3 -m ruff format --check .
50+
run: uv run ruff check && uv run ruff format --check
4151

4252
precommit_hooks:
4353
runs-on: ubuntu-latest
@@ -72,12 +82,15 @@ jobs:
7282
with:
7383
python-version: 3.11
7484

85+
- name: Install uv
86+
uses: astral-sh/setup-uv@v6
87+
with:
88+
enable-cache: true
89+
7590
- name: Install dependencies
76-
run: |
77-
python3 -m pip install --upgrade pip
78-
python3 -m pip install '.[docs]'
91+
run: uv sync --extra docs
7992

8093
- name: Attempt docs build
8194
working-directory: ./docs
82-
run: make html
95+
run: source ../.venv/bin/activate && make html
8396
{% endif %}

python/{{cookiecutter.project_slug}}/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ dependencies = [
2626
"fastapi",
2727
"pydantic~=2.1",
2828
{%- endif %}
29+
{%- if cookiecutter.add_fastapi or cookiecutter.add_cli %}
30+
"pydantic-settings",
31+
{%- endif %}
2932
]
3033
dynamic = ["version"]
3134

@@ -35,6 +38,8 @@ tests = [
3538
"pytest-cov",
3639
{%- if cookiecutter.add_fastapi %}
3740
"httpx",
41+
"jsonschema~=4.24", # pin to avoid deprecation of ref resolver
42+
"pyyaml",
3843
{%- endif %}
3944
]
4045
dev = [

python/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from importlib.metadata import version, PackageNotFoundError
44

5-
65
try:
76
__version__ = version("{{ cookiecutter.project_slug }}")
87
except PackageNotFoundError:

python/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/api.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Define API endpoints."""
22

3+
import logging
34
from collections.abc import AsyncGenerator
45
from contextlib import asynccontextmanager
56
from enum import Enum
67

78
from fastapi import FastAPI
89

910
from {{ cookiecutter.project_slug }} import __version__
10-
from {{ cookiecutter.project_slug }}.config import config
11+
from {{ cookiecutter.project_slug }}.config import get_config
1112
from {{ cookiecutter.project_slug }}.logging import initialize_logs
1213
from {{ cookiecutter.project_slug }}.models import ServiceInfo, ServiceOrganization, ServiceType
1314

@@ -20,7 +21,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator: # noqa: ARG001
2021
2122
:param app: FastAPI instance
2223
"""
23-
initialize_logs()
24+
log_level = logging.DEBUG if get_config().debug else logging.INFO
25+
26+
initialize_logs(log_level=log_level)
2427
yield
2528

2629

@@ -46,20 +49,18 @@ class _Tag(str, Enum):
4649
docs_url="/docs",
4750
openapi_url="/openapi.json",
4851
swagger_ui_parameters={"tryItOutEnabled": True},
52+
lifespan=lifespan
4953
)
5054

5155

5256
@app.get(
53-
"/service_info",
57+
"/service-info",
5458
summary="Get basic service information",
5559
description="Retrieve service metadata, such as versioning and contact info. Structured in conformance with the [GA4GH service info API specification](https://www.ga4gh.org/product/service-info/)",
5660
tags=[_Tag.META],
5761
)
5862
def service_info() -> ServiceInfo:
59-
"""Provide service info per GA4GH Service Info spec
60-
61-
:return: conformant service info description
62-
"""
63+
"""Provide service info per GA4GH Service Info spec"""
6364
return ServiceInfo(
64-
organization=ServiceOrganization(), type=ServiceType(), environment=config.env
65+
organization=ServiceOrganization(), type=ServiceType(), environment=get_config().env
6566
)

python/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Provide CLI for application."""
22

3+
import logging
4+
35
import click
46

57
from {{ cookiecutter.project_slug }} import __version__
8+
from {{ cookiecutter.project_slug }}.config import get_config
69
from {{ cookiecutter.project_slug }}.logging import initialize_logs
710

811

@@ -18,4 +21,5 @@ def cli() -> None:
1821
1922
Conclude by summarizing additional commands
2023
""" # noqa: D301
21-
initialize_logs()
24+
log_level = logging.DEBUG if get_config().debug else logging.INFO
25+
initialize_logs(log_level)
Lines changed: 21 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,37 @@
11
"""Read and provide runtime configuration."""
22

3-
import logging
4-
import os
3+
from functools import cache
54

6-
from pydantic import BaseModel
5+
from pydantic_settings import BaseSettings, SettingsConfigDict
76

87
from {{ cookiecutter.project_slug }}.models import ServiceEnvironment
98

109

11-
_logger = logging.getLogger(__name__)
10+
class Settings(BaseSettings):
11+
"""Create app settings
1212
13-
14-
_ENV_VARNAME = "{{ cookiecutter.project_slug | upper }}_ENV"
15-
16-
17-
class Config(BaseModel):
18-
"""Define app configuration data object."""
19-
20-
env: ServiceEnvironment
21-
debug: bool
22-
test: bool
23-
24-
25-
def _dev_config() -> Config:
26-
"""Provide development environment configs
27-
28-
:return: dev env configs
29-
"""
30-
return Config(env=ServiceEnvironment.DEV, debug=True, test=False)
31-
32-
33-
def _test_config() -> Config:
34-
"""Provide test env configs
35-
36-
:return: test configs
37-
"""
38-
return Config(env=ServiceEnvironment.TEST, debug=False, test=True)
39-
40-
41-
def _staging_config() -> Config:
42-
"""Provide staging env configs
43-
44-
:return: staging configs
45-
"""
46-
return Config(env=ServiceEnvironment.STAGING, debug=False, test=False)
47-
48-
49-
def _prod_config() -> Config:
50-
"""Provide production configs
51-
52-
:return: prod configs
13+
This is not a singleton, so every new call to this class will re-compute
14+
configuration settings, defaults, etc.
5315
"""
54-
return Config(env=ServiceEnvironment.PROD, debug=False, test=False)
55-
56-
57-
def _default_config() -> Config:
58-
"""Provide default configs. This function sets what they are.
5916

60-
:return: default configs
61-
"""
62-
return _dev_config()
17+
model_config = SettingsConfigDict(
18+
env_prefix="{{ cookiecutter.project_slug }}_",
19+
env_file=".env",
20+
env_file_encoding="utf-8",
21+
extra="ignore"
22+
)
6323

24+
env: ServiceEnvironment = ServiceEnvironment.DEV
25+
debug: bool = False
26+
test: bool = False
6427

65-
_CONFIG_MAP = {
66-
ServiceEnvironment.DEV: _dev_config,
67-
ServiceEnvironment.TEST: _test_config,
68-
ServiceEnvironment.STAGING: _staging_config,
69-
ServiceEnvironment.PROD: _prod_config,
70-
}
7128

29+
@cache
30+
def get_config() -> Settings:
31+
"""Get runtime configuration.
7232
73-
def _set_config() -> Config:
74-
"""Set configs based on environment variable `{{ cookiecutter.project_slug | upper }}_ENV`.
33+
This function is cached, so the config object only gets created/calculated once.
7534
76-
:return: complete config object with environment-specific parameters
35+
:return: Settings instance
7736
"""
78-
raw_env_value = os.environ.get(_ENV_VARNAME)
79-
if not raw_env_value:
80-
return _default_config()
81-
try:
82-
env_value = ServiceEnvironment(raw_env_value.lower())
83-
except ValueError:
84-
_logger.error(
85-
"Unrecognized value for %s: '%s'. Using default configs",
86-
_ENV_VARNAME,
87-
raw_env_value
88-
)
89-
return _default_config()
90-
return _CONFIG_MAP[env_value]()
91-
92-
93-
config = _set_config()
37+
return Settings()

python/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44

55

6-
def initialize_logs(log_level: int = logging.DEBUG) -> None:
6+
def initialize_logs(log_level: int = logging.INFO) -> None:
77
"""Configure logging.
88
99
:param log_level: app log level to set

python/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ class ServiceInfo(BaseModel):
5353
"https://github.com/{{ cookiecutter.org }}/{{ cookiecutter.repo }}"
5454
)
5555
createdAt: Literal["{% now 'utc', '%Y-%m-%dT%H:%M:%S+00:00' %}"] = "{% now 'utc', '%Y-%m-%dT%H:%M:%S+00:00' %}" # noqa: N815
56-
updatedAt: str | None = None # noqa: N815
56+
updatedAt: Literal["{% now 'utc', '%Y-%m-%dT%H:%M:%S+00:00' %}"] = "{% now 'utc', '%Y-%m-%dT%H:%M:%S+00:00' %}" # noqa: N815
5757
environment: ServiceEnvironment
5858
version: Literal[__version__] = __version__

python/{{cookiecutter.project_slug}}/tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
17
def pytest_addoption(parser):
28
"""Add custom commands to pytest invocation.
39
@@ -20,3 +26,9 @@ def pytest_configure(config):
2026
# logging.getLogger("botocore").setLevel(logging.ERROR)
2127
# logging.getLogger("boto3").setLevel(logging.ERROR)
2228
# logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
29+
30+
31+
@pytest.fixture(scope="session")
32+
def test_data_dir() -> Path:
33+
"""Provide Path instance pointing to test data directory"""
34+
return Path(__file__).parent / "data"

0 commit comments

Comments
 (0)