Skip to content

Commit 2d86c93

Browse files
semperventwatsoncl1Grant, Josh
authored
Release/1.0.1 (#1)
* Fix CHANGELOG link and bump version * address ruff-discovered issues * ruff format --------- Co-authored-by: Cory Watson <watsoncl1@ornl.gov> Co-authored-by: Grant, Josh <grantjn@ornl.gov>
1 parent 6709084 commit 2d86c93

19 files changed

Lines changed: 294 additions & 203 deletions

.github/workflows/lint.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Lint with Ruff
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
7+
branches:
8+
- main
9+
workflow_dispatch:
10+
11+
jobs:
12+
ruff-lint:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: astral-sh/ruff-action@v3
17+
with:
18+
args: check
19+
- uses: astral-sh/ruff-action@v3
20+
with:
21+
args: format --check

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,25 @@ This project adheres to [Semantic Versioning](https://semver.org/).
4040

4141
## [1.0.0] - 2025-05-09
4242
- Initial commit
43+
44+
45+
## [1.0.1] - 2025-05-09
46+
### Added
47+
- 🔥 pytest as a dev dependency, used for running tests
48+
- 🔥 pytest-cov as a dev dependency, used for measuring test coverage
49+
- 🔥 pytest-mock as a dev dependency, used for mocking in tests
50+
- 🔥 added a multitude of linting rules
51+
- 🔥 added a lint and format checking GitHub Action
52+
- 🔥 added development instructions in the README
53+
54+
### Fixed
55+
- 🩹 Fix URL for the CHANGELOG
56+
- 🩹 Reformatted files to be ruff format compliant
57+
- 🩹 Fixed some typing annotation errors
58+
59+
### Removed
60+
- 🗡️ Remove version specification in README.md
61+
- 🗡️ Remove references to old indexes in pyproject.toml
62+
- 🗡️ Remove kwargs in `__init__` method of `BaseProbe`
63+
- 🗡️ Remove unused imports from many files
64+
- 🗡️ Remove commented-out code in some classes

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ python tools for adding clock data to a timescale db.
1111
1. Ensure you have Python 3.9 or higher installed
1212
2. Pip install the latest version of opensampl:
1313
```bash
14-
pip install opensampl==0.2.0
14+
pip install opensampl
1515
```
1616

17+
### Development Setup
18+
```bash
19+
uv venv
20+
uv sync --extra all
21+
source .venv/bin/activate
22+
```
23+
This will create a virtual environment and install the development dependencies.
24+
1725
### Environment Setup
1826

1927
The tool requires several environment variables. Create a `.env` file in your project root:

opensampl/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""
2+
Initialize the openSAMPL package.
3+
24
Adding this to ensure backwards compatibility with older versions of opensampl where orm import was:
35
from opensampl import orm
46
"""

opensampl/cli.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""
2+
Define the command line interface for openSAMPL.
3+
24
The openSAMPL CLI package is a click based command line interface for the openSAMPL package. It provides a way to
35
interact with the database and load data into it.
46
"""
57

68
import json
79
import sys
810
from pathlib import Path
9-
from typing import Dict, List, Union
11+
from typing import Optional, Union
1012

1113
import click
1214
import yaml
@@ -19,7 +21,7 @@
1921
from opensampl.load_data import create_new_tables, write_to_table
2022
from opensampl.vendors.constants import VENDOR_MAP, get_vendor_parser
2123

22-
BANNER=r"""
24+
BANNER = r"""
2325
2426
____ _ __ __ ____ _
2527
___ _ __ ___ _ __ / ___| / \ | \/ | _ \| |
@@ -35,10 +37,11 @@
3537
level = ENV_VARS.LOG_LEVEL.get_value()
3638
logger.configure(handlers=[{"sink": sys.stderr, "level": level.upper()}])
3739

40+
3841
class CaseInsensitiveGroup(click.Group):
3942
"""Defines Click group options as case-insensitive. By default, click groups are case-sensitive."""
4043

41-
def get_command(self, ctx, cmd_name):
44+
def get_command(self, ctx, cmd_name: str) -> Optional[click.Command]: # noqa: ARG002,ANN001
4245
"""Normalize command name to lower case"""
4346
cmd_name = cmd_name.lower()
4447
# Match against lowercased command names
@@ -47,6 +50,7 @@ def get_command(self, ctx, cmd_name):
4750
return cmd
4851
return None
4952

53+
5054
def get_table_names():
5155
"""Get all table names from the ORM in opensampl.db.orm"""
5256
return [table.name for table in Base.metadata.sorted_tables]
@@ -73,6 +77,7 @@ def init():
7377
def config():
7478
"""View and manage environment variables used by openSAMPL"""
7579

80+
7681
@config.command()
7782
def file():
7883
"""Show the path to the env file used by openSAMPL"""
@@ -123,7 +128,7 @@ def show(explain: bool, var: str):
123128
@config.command("set")
124129
@click.argument("name")
125130
@click.argument("value")
126-
def config_set(name: str, value: str, temp: bool = None):
131+
def config_set(name: str, value: str, temp: Optional[bool] = None):
127132
"""
128133
Set the value of an environment variable.
129134
@@ -137,25 +142,25 @@ def config_set(name: str, value: str, temp: bool = None):
137142
"""
138143
set_env(name=name, value=value, temp=temp)
139144

145+
140146
@cli.group(cls=CaseInsensitiveGroup)
141147
def load():
142148
"""Load data into database"""
143149

144150

145-
for probe_name, vendor_type in VENDOR_MAP.items():
151+
for probe_name in VENDOR_MAP:
146152
load.add_command(get_vendor_parser(probe_name).get_cli_command(), name=probe_name)
147153

148154

149-
def path_or_string(value):
155+
def path_or_string(value: str) -> Union[dict, list]:
150156
"""Get content from a file or use the string directly"""
151157
# Get content - either from file or use the string directly
152158
content = value
153159
try:
154160
path = Path(value)
155161
if path.exists() and path.is_file():
156-
with open(path, "r") as f:
157-
content = f.read()
158-
except Exception:
162+
content = path.read_text()
163+
except Exception: # noqa: S110
159164
# If any error occurs during path handling, treat as raw string
160165
pass
161166

@@ -169,8 +174,8 @@ def path_or_string(value):
169174
except json.JSONDecodeError as json_err:
170175
# If both parsing attempts fail, raise an error
171176
raise click.BadParameter(
172-
f"Could not parse input as YAML or JSON.\n" f"YAML error: {yaml_err}\n" f"JSON error: {json_err}"
173-
)
177+
f"Could not parse input as YAML or JSON.\nYAML error: {yaml_err}\nJSON error: {json_err}"
178+
) from json_err
174179

175180

176181
@load.command("table")
@@ -183,8 +188,10 @@ def path_or_string(value):
183188
)
184189
@click.argument("table_name", type=click.Choice(get_table_names()))
185190
@click.argument("filepath", type=path_or_string)
186-
def table_load(filepath: Union[Dict, List], table_name: str, if_exists: str):
191+
def table_load(filepath: Union[dict, list], table_name: str, if_exists: str):
187192
r"""
193+
Perform a Table load into the database.
194+
188195
Load data directly into a database table. Format can be yaml or json. Can be a list of dictionaries or a single
189196
dictionary.
190197
@@ -208,8 +215,8 @@ def table_load(filepath: Union[Dict, List], table_name: str, if_exists: str):
208215
write_to_table(table_name, filepath, if_exists=if_exists)
209216
click.echo(f"Successfully wrote data to table {table_name}")
210217
except Exception as e:
211-
click.echo(f"Error writing to table: {str(e)}", err=True)
212-
raise click.Abort()
218+
click.echo(f"Error writing to table: {e!s}", err=True)
219+
raise click.Abort() # noqa: RSE102,B904
213220

214221

215222
@cli.command(name="create")
@@ -221,10 +228,7 @@ def table_load(filepath: Union[Dict, List], table_name: str, if_exists: str):
221228
help="Update the database with the new probe type",
222229
)
223230
def create_probe_command(config_path: Path, update_db: bool):
224-
"""
225-
** beta **
226-
Create a new probe type with scaffolding, based on a config file.
227-
"""
231+
"""Create a new probe type with scaffolding, based on a config file."""
228232
from opensampl.helpers.create_vendor import VendorConfig
229233

230234
vendor_config = VendorConfig.from_config_file(config_path)

opensampl/constants.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Constants for accessing environment configurations"""
2+
23
import os
34
from pathlib import Path
4-
from typing import Any, Optional, Type
5+
from typing import Any, Optional
56

67
from pydantic import BaseModel
78

@@ -11,35 +12,36 @@ class EnvVar(BaseModel):
1112

1213
name: str
1314
description: str
14-
type: Type = str
15+
type: type = str
1516
default: Optional[Any] = None
1617

1718
def get_value(self):
1819
"""Get value of var in environment"""
1920
default = self.resolve_default()
2021
if self.type is bool:
21-
return os.getenv(self.name, default).lower() == 'true'
22-
elif self.type is Path:
22+
return os.getenv(self.name, default).lower() == "true"
23+
if self.type is Path:
2324
return Path(os.getenv(self.name, default))
2425
return os.getenv(self.name, default)
2526

2627
def resolve_default(self):
2728
"""Resolve default value for env var based on type"""
2829
if self.type is bool:
29-
return self.default or 'false'
30+
return self.default or "false"
3031
if self.type is Path and self.default is not None:
3132
return Path(self.default).resolve()
3233
return self.default
3334

3435

35-
class ENV_VARS:
36+
class ENV_VARS: # noqa: N801
3637
"""Variables referenced by openSAMPL"""
3738

3839
ROUTE_TO_BACKEND = EnvVar(
3940
name="ROUTE_TO_BACKEND",
40-
description=("Route all database operations through BACKEND_URL rather "
41-
"than applying directly using DATABASE_URL"),
42-
type=bool
41+
description=(
42+
"Route all database operations through BACKEND_URL rather than applying directly using DATABASE_URL"
43+
),
44+
type=bool,
4345
)
4446
BACKEND_URL = EnvVar(
4547
name="BACKEND_URL",
@@ -66,12 +68,12 @@ class ENV_VARS:
6668
)
6769

6870
@classmethod
69-
def __iter__(cls):
71+
def __iter__(cls) -> iter:
7072
"""Get all EnvVar objects as iterable"""
7173
yield from (value for key, value in cls.__dict__.items() if isinstance(value, EnvVar))
7274

7375
@classmethod
74-
def get(cls, name: str):
76+
def get(cls, name: str) -> Optional[Any]:
7577
"""Get EnvVar object by name"""
7678
var = getattr(cls, name, None)
7779
if isinstance(var, EnvVar):

opensampl/db/access_orm.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""ORM for managing access to openSAMPL database and endpoints"""
2+
23
import secrets
34
import uuid
4-
from datetime import datetime
5-
from typing import List, Union
5+
from datetime import datetime, timezone
6+
from typing import Optional, Union
67

78
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
8-
from sqlalchemy.orm import declarative_base, relationship
9+
from sqlalchemy.orm import Session, declarative_base, relationship
910
from sqlalchemy.orm.exc import NoResultFound
1011
from sqlalchemy.schema import MetaData
1112

@@ -29,7 +30,7 @@ def generate_key(self):
2930

3031
def is_expired(self):
3132
"""Check if API access key is expired."""
32-
return self.expires_at is not None and datetime.utcnow() > self.expires_at
33+
return self.expires_at is not None and datetime.now(tz=timezone.utc) > self.expires_at
3334

3435

3536
class Views(Base):
@@ -40,12 +41,12 @@ class Views(Base):
4041
name = Column(Text)
4142

4243
@staticmethod
43-
def get_view_by_name(session, name):
44+
def get_view_by_name(session: Session, name: str) -> Optional[type["Views"]]:
4445
"""Get view by name"""
4546
try:
4647
return session.query(Views).filter_by(name=name).one()
4748
except NoResultFound:
48-
print(f"View with name {name} not found")
49+
print(f"View with name {name} not found") # noqa: T201
4950
return None
5051

5152

@@ -58,12 +59,12 @@ class Roles(Base):
5859
view_id = Column(Text, ForeignKey("views.view_id"))
5960

6061
@staticmethod
61-
def get_role_by_name(session, name):
62+
def get_role_by_name(session: Session, name: str) -> Optional[type["Roles"]]:
6263
"""Get role by name"""
6364
try:
6465
return session.query(Roles).filter_by(name=name).one()
6566
except NoResultFound:
66-
print(f"Role with name {name} not found")
67+
print(f"Role with name {name} not found") # noqa: T201
6768
return None
6869

6970

@@ -75,12 +76,12 @@ class Users(Base):
7576
email = Column(Text)
7677

7778
@staticmethod
78-
def get_user_by_email(session, email):
79+
def get_user_by_email(session: Session, email: str) -> Optional["Users"]:
7980
"""Get user by email"""
8081
try:
8182
return session.query(Users).filter_by(email=email).one()
8283
except NoResultFound:
83-
print(f"User with email {email} not found")
84+
print(f"User with email {email} not found") # noqa: T201
8485
return None
8586

8687

@@ -92,14 +93,14 @@ class UserRole(Base):
9293
role_id = Column(Text, ForeignKey("roles.role_id"), primary_key=True)
9394

9495

95-
def add_user_role(emails: Union[str, List[str]], role_name: str, session):
96+
def add_user_role(emails: Union[str, list[str]], role_name: str, session: Session):
9697
"""Add user role to the database."""
9798
if isinstance(emails, str):
9899
emails = [emails]
99100

100101
role = Roles.get_role_by_name(name=role_name)
101102
if role is None:
102-
print(f"Role with name {role_name} not found")
103+
print(f"Role with name {role_name} not found") # noqa: T201
103104
return
104105

105106
for email in emails:
@@ -109,17 +110,17 @@ def add_user_role(emails: Union[str, List[str]], role_name: str, session):
109110
user = Users(email=email)
110111
session.add(user)
111112
session.flush() # Flush to get the generated user_id
112-
print(f"New user created with email {email}")
113+
print(f"New user created with email {email}") # noqa: T201
113114

114115
# Check if user already has the specified role
115116
if any(ur.role_id == role.role_id for ur in user.user_role):
116-
print(f"User with email {email} already has role {role_name}")
117+
print(f"User with email {email} already has role {role_name}") # noqa: T201
117118
continue
118119

119120
# Create a new entry in user_role table
120121
user_role = UserRole(user_id=user.user_id, role_id=role.role_id)
121122
session.add(user_role)
122-
print(f"User with email {email} assigned role {role_name}")
123+
print(f"User with email {email} assigned role {role_name}") # noqa: T201
123124
session.commit()
124125

125126

0 commit comments

Comments
 (0)