Skip to content

Commit 897b498

Browse files
semperventGrant, Josh
andauthored
Release/1.0.2 (#2)
* type checking, ruff checking, and ruff formatting * add in Changelog changes for this release * finish with a newline --------- Co-authored-by: Grant, Josh <grantjn@ornl.gov>
1 parent 2d86c93 commit 897b498

12 files changed

Lines changed: 106 additions & 54 deletions

File tree

.github/workflows/lint.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ jobs:
1919
- uses: astral-sh/ruff-action@v3
2020
with:
2121
args: format --check
22+
- name: Install uv
23+
uses: astral-sh/setup-uv@v5
24+
with:
25+
version: "0.7.3"
26+
- name: Set up Python
27+
run: uv python install
28+
- name: Sync project, including dev dependencies
29+
run: uv sync
30+
- name: Build static content
31+
run: |
32+
uv run python ./scripts/gen_api_docs.py
33+
uv run mkdocs build --clean

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,14 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6262
- 🗡️ Remove kwargs in `__init__` method of `BaseProbe`
6363
- 🗡️ Remove unused imports from many files
6464
- 🗡️ Remove commented-out code in some classes
65+
66+
## [1.0.2] - 2025-05-12
67+
### Added
68+
- 🔥 `black` added as a dependency for auto-creation of probe types
69+
- 🔥 `ty` added as a dependency for type-checking
70+
71+
### Fixed
72+
- 🩹 `ty` type checking errors addressed
73+
74+
### Changed
75+
- ⚡ linting github action now confirms that the documentation can be built

opensampl/cli.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import json
99
import sys
1010
from pathlib import Path
11-
from typing import Optional, Union
11+
from typing import Literal, Optional, Union
1212

1313
import click
1414
import yaml
@@ -34,7 +34,7 @@
3434

3535
env_file = find_dotenv()
3636
load_dotenv()
37-
level = ENV_VARS.LOG_LEVEL.get_value()
37+
level = str(ENV_VARS.LOG_LEVEL.get_value())
3838
logger.configure(handlers=[{"sink": sys.stderr, "level": level.upper()}])
3939

4040

@@ -128,7 +128,7 @@ def show(explain: bool, var: str):
128128
@config.command("set")
129129
@click.argument("name")
130130
@click.argument("value")
131-
def config_set(name: str, value: str, temp: Optional[bool] = None):
131+
def config_set(name: str, value: str):
132132
"""
133133
Set the value of an environment variable.
134134
@@ -140,7 +140,7 @@ def config_set(name: str, value: str, temp: Optional[bool] = None):
140140
opensampl config set BACKEND_URL http://localhost:8000
141141
142142
"""
143-
set_env(name=name, value=value, temp=temp)
143+
set_env(name=name, value=value)
144144

145145

146146
@cli.group(cls=CaseInsensitiveGroup)
@@ -188,7 +188,9 @@ def path_or_string(value: str) -> Union[dict, list]:
188188
)
189189
@click.argument("table_name", type=click.Choice(get_table_names()))
190190
@click.argument("filepath", type=path_or_string)
191-
def table_load(filepath: Union[dict, list], table_name: str, if_exists: str):
191+
def table_load(
192+
filepath: Union[dict, list], table_name: str, if_exists: Literal["update", "error", "replace", "ignore"]
193+
):
192194
r"""
193195
Perform a Table load into the database.
194196

opensampl/constants.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Constants for accessing environment configurations"""
22

33
import os
4+
from collections.abc import Iterator
45
from pathlib import Path
5-
from typing import Any, Optional
6+
from typing import Any, Optional, Union
67

78
from pydantic import BaseModel
89

@@ -12,23 +13,23 @@ class EnvVar(BaseModel):
1213

1314
name: str
1415
description: str
15-
type: type = str
16+
type_: type = str
1617
default: Optional[Any] = None
1718

18-
def get_value(self):
19+
def get_value(self) -> Optional[Union[str, bool, Path]]:
1920
"""Get value of var in environment"""
2021
default = self.resolve_default()
21-
if self.type is bool:
22+
if self.type_ is bool:
2223
return os.getenv(self.name, default).lower() == "true"
23-
if self.type is Path:
24+
if self.type_ is Path:
2425
return Path(os.getenv(self.name, default))
2526
return os.getenv(self.name, default)
2627

27-
def resolve_default(self):
28+
def resolve_default(self) -> Optional[Union[str, bool, Path]]:
2829
"""Resolve default value for env var based on type"""
29-
if self.type is bool:
30+
if self.type_ is bool:
3031
return self.default or "false"
31-
if self.type is Path and self.default is not None:
32+
if self.type_ is Path and self.default is not None:
3233
return Path(self.default).resolve()
3334
return self.default
3435

@@ -41,7 +42,7 @@ class ENV_VARS: # noqa: N801
4142
description=(
4243
"Route all database operations through BACKEND_URL rather than applying directly using DATABASE_URL"
4344
),
44-
type=bool,
45+
type_=bool,
4546
)
4647
BACKEND_URL = EnvVar(
4748
name="BACKEND_URL",
@@ -54,7 +55,7 @@ class ENV_VARS: # noqa: N801
5455
ARCHIVE_PATH = EnvVar(
5556
name="ARCHIVE_PATH",
5657
description="Default path that files are moved to after they have been processed",
57-
type=Path,
58+
type_=Path,
5859
default="archive",
5960
)
6061
LOG_LEVEL = EnvVar(
@@ -68,7 +69,7 @@ class ENV_VARS: # noqa: N801
6869
)
6970

7071
@classmethod
71-
def __iter__(cls) -> iter:
72+
def __iter__(cls) -> Iterator[EnvVar]:
7273
"""Get all EnvVar objects as iterable"""
7374
yield from (value for key, value in cls.__dict__.items() if isinstance(value, EnvVar))
7475

opensampl/db/orm.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import uuid
44
from datetime import datetime
5-
from typing import Any
5+
from typing import Any, Optional
66

77
from geoalchemy2 import Geometry, WKTElement
88
from geoalchemy2.shape import to_shape
@@ -101,7 +101,7 @@ class ProbeMetadata(Base):
101101
probe_data = relationship("ProbeData")
102102
adva_metadata = relationship("AdvaMetadata", back_populates="probe", uselist=False)
103103

104-
def __init__(self, **kwargs: dict):
104+
def __init__(self, **kwargs: Any):
105105
"""Initialize Probe Metadata object, dealing with converting location name into uuid"""
106106
location_name = kwargs.pop("location_name", None)
107107
test_name = kwargs.pop("test_name", None)
@@ -112,7 +112,7 @@ def __init__(self, **kwargs: dict):
112112
if test_name:
113113
self._test_name = test_name
114114

115-
def resolve_references(self, session: Session = None):
115+
def resolve_references(self, session: Optional[Session] = None):
116116
"""
117117
Resolve references.
118118

opensampl/helpers/create_vendor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def generate_default_fields(cls, data: Any) -> Any:
6969
fields = []
7070
for field, sql_type in metadata_fields.items():
7171
if sql_type:
72-
fields.append(MetadataField(name=field, type=sql_type))
72+
fields.append(MetadataField(name=field, sqlalchemy_type=sql_type))
7373
else:
7474
fields.append(MetadataField(name=field))
7575
data["metadata_fields"] = fields
@@ -239,7 +239,8 @@ def update_constants(self):
239239

240240
file_text = constants_path.read_text()
241241
tree = ast.parse(file_text)
242-
242+
vm_lineno = None
243+
vm_end_lineno = None
243244
for _, node in enumerate(tree.body):
244245
if (
245246
isinstance(node, ast.Assign)

opensampl/helpers/source_writer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Helper for writing new source code based on new Vendor Configuration"""
22

3+
from typing import Any
4+
35
import astor
46
import libcst as cst
57
from black import FileMode, format_str
@@ -54,7 +56,7 @@ def leave_ClassDef( # noqa: N802
5456
return updated_node.with_changes(body=updated_node.body.with_changes(body=body))
5557

5658
@classmethod
57-
def format(cls, tree: cst.Module) -> cst.Module:
59+
def format(cls, tree: Any) -> cst.Module:
5860
"""Convert back to source, use black to format"""
5961
source = format_str(
6062
astor.to_source(tree),

opensampl/load_data.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pandas as pd
88
import requests
9+
import requests.exceptions
910
from loguru import logger
1011
from sqlalchemy import UniqueConstraint, and_, create_engine, inspect, text
1112
from sqlalchemy.orm import Session, sessionmaker
@@ -32,7 +33,7 @@ def route_or_direct(route_endpoint: str, method: request_methods = "POST", send_
3233

3334
def decorator(func: Callable) -> Callable:
3435
@wraps(func)
35-
def wrapper(*args: list, **kwargs: dict) -> Callable:
36+
def wrapper(*args: list, **kwargs: dict) -> Optional[Callable]:
3637
session = kwargs.pop("session", None)
3738
backend_url = ENV_VARS.BACKEND_URL.get_value()
3839
route_to_backend = ENV_VARS.ROUTE_TO_BACKEND.get_value()
@@ -80,7 +81,7 @@ def wrapper(*args: list, **kwargs: dict) -> Callable:
8081
"Provide Session or Set DATABASE_URL env var to use direct database operations or set "
8182
"ROUTE_TO_BACKEND to true and configure BACKEND_URL to use backend routing"
8283
)
83-
session = sessionmaker(create_engine(database_url))()
84+
session = sessionmaker(create_engine(database_url))() # ty: ignore[no-matching-overload]
8485

8586
return func(*args, **kwargs, session=session)
8687

@@ -116,6 +117,8 @@ def write_to_table(
116117
SQLAlchemyError: For database errors
117118
118119
"""
120+
if not isinstance(session, Session):
121+
raise TypeError("Session must be a SQLAlchemy session")
119122
if if_exists not in ["error", "replace", "update", "ignore"]:
120123
raise ValueError("on_conflict must be one of: 'error', 'replace', 'update', 'ignore'")
121124

@@ -202,7 +205,7 @@ def build_pk_conditions(
202205
return pk_conditions
203206

204207

205-
def extract_unique_constraints(inspector: inspect, data: dict[str, Any]):
208+
def extract_unique_constraints(inspector: Any, data: dict[str, Any]):
206209
"""
207210
Identify unique constraints that can be used to match existing entries.
208211
@@ -248,17 +251,19 @@ def find_existing_entry(
248251
249252
"""
250253
if pk_conditions:
251-
existing = session.query(TableModel).filter(and_(*pk_conditions)).first()
254+
existing = session.query(TableModel).filter(and_(*pk_conditions)).first() # ty: ignore[missing-argument]
252255
if existing:
253256
return existing
254257

255258
all_constraints = []
256259
for constraint_columns in unique_constraints:
257-
constraint_condition = and_(*(getattr(TableModel, col) == val for col, val in constraint_columns))
260+
constraint_condition = and_( # ty: ignore[missing-argument]
261+
*(getattr(TableModel, col) == val for col, val in constraint_columns)
262+
)
258263
all_constraints.append(constraint_condition)
259264

260265
if all_constraints:
261-
return session.query(TableModel).filter(and_(*all_constraints)).first()
266+
return session.query(TableModel).filter(and_(*all_constraints)).first() # ty: ignore[missing-argument]
262267

263268
return None
264269

@@ -268,7 +273,7 @@ def handle_existing_entry( # noqa: PLR0913
268273
TableModel, # noqa: N803, ANN001
269274
data: dict[str, Any],
270275
pk_columns: list[str],
271-
inspector: inspect,
276+
inspector: Any,
272277
if_exists: conflict_actions,
273278
session: Optional[Session],
274279
):
@@ -322,6 +327,8 @@ def handle_existing_entry( # noqa: PLR0913
322327
@route_or_direct("load_time_data", send_file=True)
323328
def load_time_data(probe_key: ProbeKey, data: pd.DataFrame, session: Optional[Session] = None):
324329
"""Load time series data"""
330+
if not isinstance(session, Session):
331+
raise TypeError("Session must be a SQLAlchemy session")
325332
route_to_backend = ENV_VARS.ROUTE_TO_BACKEND.get_value()
326333
if route_to_backend:
327334
csv_data = data.to_csv(index=False).encode("utf-8")
@@ -352,7 +359,7 @@ def load_time_data(probe_key: ProbeKey, data: pd.DataFrame, session: Optional[Se
352359
# Ensure correct dtypes
353360
df = df.astype({"time": "datetime64[ns]", "value": "float64", "probe_uuid": str})
354361

355-
dtype = {column.name: column.type for column in ProbeData.__table__.columns}
362+
dtype = {column.name: column.type_ for column in ProbeData.__table__.columns}
356363

357364
# Write directly to database using pandas
358365
df.to_sql(
@@ -380,6 +387,8 @@ def load_probe_metadata(
380387
session: Optional[Session] = None,
381388
):
382389
"""Write object to table"""
390+
if not isinstance(session, Session):
391+
raise TypeError("Session must be a SQLAlchemy session")
383392
route_to_backend = ENV_VARS.ROUTE_TO_BACKEND.get_value()
384393
if route_to_backend:
385394
return {
@@ -419,6 +428,8 @@ def load_probe_metadata(
419428
@route_or_direct("create_new_tables", method="GET")
420429
def create_new_tables(create_schema: bool = True, session: Optional[Session] = None):
421430
"""Use the ORM definition to create all tables, optionally creating the schema as well"""
431+
if not isinstance(session, Session):
432+
raise TypeError("Session must be a SQLAlchemy session")
422433
route_to_backend = ENV_VARS.ROUTE_TO_BACKEND.get_value()
423434
if route_to_backend:
424435
return {"create_schema": create_schema}

opensampl/server/cli.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import shlex
88
import subprocess
99
import sys
10+
from typing import TextIO, cast
1011

1112
import click
1213
from dotenv import load_dotenv
@@ -51,7 +52,7 @@ def get_cast_compose_file():
5152
with pkg_resources.path("opensampl.server", filename) as path:
5253
return str(path)
5354
except ImportError:
54-
click.echo("Error: docker-compose.yaml file not found in package.", error=True)
55+
click.echo("Error: docker-compose.yaml file not found in package.", err=True)
5556
sys.exit(1)
5657

5758

@@ -89,10 +90,10 @@ def up(env_file: click.Path | str, extra_args: list):
8990

9091
logger.debug(f"Running: {command}")
9192
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) # noqa: S603
92-
93-
for line in process.stdout:
94-
print(line, end="") # noqa: T201 Print each line as it arrives
95-
93+
stdout = cast("TextIO", process.stdout)
94+
if stdout:
95+
for line in stdout:
96+
print(line, end="") # noqa: T201 Print each line as it arrives
9697
process.wait()
9798

9899
set_env(name="BACKEND_URL", value="http://localhost:8015")
@@ -122,9 +123,10 @@ def down(env_file: click.Path, extra_args: list) -> None:
122123
if extra_args:
123124
command.extend(extra_args)
124125
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) # noqa: S603
125-
126-
for line in process.stdout:
127-
print(line, end="") # noqa: T201 Print each line as it arrives
126+
stdout = cast("TextIO", process.stdout)
127+
if stdout:
128+
for line in stdout:
129+
print(line, end="") # noqa: T201 Print each line as it arrives
128130

129131
process.wait()
130132

@@ -155,10 +157,10 @@ def ps(env_file: click.Path) -> None:
155157
command = build_docker_compose_base(env_file)
156158
command.extend(["ps"])
157159
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) # noqa: S603
158-
159-
for line in process.stdout:
160-
print(line, end="") # noqa: T201 Print each line as it arrives
161-
160+
stdout = cast("TextIO", process.stdout)
161+
if stdout:
162+
for line in stdout:
163+
print(line, end="") # noqa: T201 Print each line as it arrives
162164
process.wait()
163165

164166

@@ -178,9 +180,10 @@ def run(env_file: click.Path, run_commands: list) -> None:
178180
command.extend(list(run_commands))
179181
logger.info(command)
180182
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) # noqa: S603
181-
182-
for line in process.stdout:
183-
print(line, end="") # noqa: T201 Print each line as it arrives
183+
stdout = cast("TextIO", process.stdout)
184+
if stdout:
185+
for line in stdout:
186+
print(line, end="") # noqa: T201 Print each line as it arrives
184187

185188
process.wait()
186189

0 commit comments

Comments
 (0)