Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dependency-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [bugfix/dependabot]
branches: [main]
schedule:
- cron: "17 9 * * 1"
workflow_dispatch:
Expand Down
31 changes: 20 additions & 11 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Lint with Ruff

on:
push:
branches:
Expand All @@ -8,29 +9,37 @@ on:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
ruff-lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.12.21"
version: "0.13.5"
enable-cache: true

- name: Set up Python
run: uv python install

- name: Sync project, including dev dependencies
run: uv sync --locked --all-extras --all-groups
- uses: astral-sh/ruff-action@v3
with:
version: "0.12.12"
args: check
- uses: astral-sh/ruff-action@v3
with:
args: format --check

- name: Check linting with Ruff
run: uv run --no-sync ruff check .

- name: Check formatting with Ruff
run: uv run --no-sync ruff format --check .

- name: Build static content
run: |
uv run python ./scripts/gen_api_docs.py
uv run mkdocs build --clean
uv run --no-sync python ./scripts/gen_api_docs.py
uv run --no-sync mkdocs build --clean
test -s site/search/search_index.json
test -n "$(find site/assets/javascripts -type f -name '*.js' -print -quit)"
9 changes: 8 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v7
Expand All @@ -30,6 +30,13 @@ jobs:

- name: Install PostgreSQL and PostGIS tooling
run: |
sudo install -d /usr/share/postgresql-common/pgdg

sudo curl -fsSL \
https://www.postgresql.org/media/keys/ACCC4CF8.asc \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt resolute-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update
sudo apt-get install -y postgresql-16 postgresql-client-16 postgresql-16-postgis-3 postgresql-16-postgis-3-scripts
echo "/usr/lib/postgresql/16/bin" >> "$GITHUB_PATH"
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,26 @@ This project adheres to [Semantic Versioning](https://semver.org/).


-->
## [1.2.6] = 2026-10-26
### Security
- πŸ” Remediated Dependabot alerts by raising python minor and patch groups
- πŸ” Bumps the container-minor-and-patch group with 1 update in the / directory: astral-sh/uv.
- πŸ” Updates astral-sh/uv from 0.13.1 to 0.13.4 in the docs Dockerfile
- πŸ” Updates sqlalchemy from 2.0.54 to 2.0.55
- πŸ” Updates fastapi from 0.143.3 to 0.146.0
- πŸ” Updates pytest-mock from 3.16.0 to 3.17.0
- πŸ” Updates telnet3 from 2.0.4 to 5.0.1
- πŸ” Updates Pillow to 13.0.0

### Changed
- ⚑ updated `OPENSAMPL_VERSION` in the Dockerfiles
- ⚑ updated workflows so Ubuntu 26.0.4 has access to Postgres 16
- ⚑ updated Ruff in lint action to 0.17.1

### Removed
- πŸ—‘οΈ Removed support for EOL Python 3.10


## [1.2.5] = 2026-10-21
### Security
- πŸ” Remediated Dependabot alerts by raising python minor and patch groups
Expand Down
2 changes: 1 addition & 1 deletion docs.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:0.13.1 AS uv
FROM ghcr.io/astral-sh/uv:0.13.4 AS uv

FROM python:3.14 AS builder
WORKDIR /app
Expand Down
10 changes: 2 additions & 8 deletions opensampl/collect/microchip/tp4100/collect_4100.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
See the user guide for how to configure access to the web interface.
"""

import sys
import textwrap
import warnings
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from pprint import pformat
from typing import Any, Literal
Expand All @@ -22,11 +21,6 @@
from opensampl.collect.microchip.tp4100 import DEFAULT_MONITOR_CONFIG, MetricInfo, MonitoringConfig
from opensampl.config.tp4100 import TP4100Config

if sys.version_info >= (3, 11):
from datetime import UTC
else:
UTC = timezone.utc

warnings.filterwarnings("ignore", category=InsecureRequestWarning)


Expand Down Expand Up @@ -316,7 +310,7 @@ def download_files(
if key.strip() == "Title":
title_val, rest = val.split("(", maxsplit=1)
headers[key.strip()] = title_val
metr_str, inner_info = rest.split("):", maxsplit=1)
_metr_str, inner_info = rest.split("):", maxsplit=1)
headers.update(
{k.strip(): v for k, v in (x.split(" = ", maxsplit=1) for x in inner_info.split(", "))}
)
Expand Down
4 changes: 2 additions & 2 deletions opensampl/collect/microchip/twst/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import re
import textwrap
from datetime import datetime, timezone
from datetime import UTC, datetime
from types import SimpleNamespace
from typing import Any

Expand Down Expand Up @@ -145,7 +145,7 @@ async def get_context(self):
and remote station tracking data.
"""
async with self.connect():
self.result.timestamp = datetime.now(tz=timezone.utc).isoformat() + "Z"
self.result.timestamp = datetime.now(tz=UTC).isoformat() + "Z"
self.result.local = SimpleNamespace()
self.result.remotes = {}

Expand Down
4 changes: 2 additions & 2 deletions opensampl/collect/microchip/twst/readings.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def reader_task(self):
if not line:
break # EOF
await self.queue.put(line)
except asyncio.TimeoutError:
except TimeoutError:
logger.debug(f"Timeout waiting for data from {self.host}:{self.port}")
finally:
await self.queue.put(SENTINEL)
Expand Down Expand Up @@ -105,7 +105,7 @@ async def processor_task(self):
break
parsed = self.parse_line(line)
if parsed:
timestamp, definition, value = parsed
_timestamp, definition, _value = parsed
if self.should_keep(definition):
self.readings.append(parsed)
finally:
Expand Down
4 changes: 2 additions & 2 deletions opensampl/db/access_orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import secrets
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional

from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
Expand Down Expand Up @@ -42,7 +42,7 @@ def is_expired(self):
True if the key is expired, False otherwise.

"""
return self.expires_at is not None and datetime.now(tz=timezone.utc) > self.expires_at
return self.expires_at is not None and datetime.now(tz=UTC) > self.expires_at


class Views(Base):
Expand Down
23 changes: 17 additions & 6 deletions opensampl/helpers/geolocator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
from __future__ import annotations

import ipaddress
import json
import os
import socket
import urllib.request
from typing import TYPE_CHECKING

import requests
from loguru import logger

from opensampl.load.table_factory import TableFactory
Expand Down Expand Up @@ -44,11 +43,23 @@ def _is_private_or_loopback(ip: str) -> bool:
def _lookup_geo_ipapi(ip: str) -> tuple[float, float, str] | None:
if ip in _GEO_CACHE:
return _GEO_CACHE[ip]
url = f"http://ip-api.com/json/{ip}?fields=status,lat,lon,city,country"

try:
with urllib.request.urlopen(url, timeout=4.0) as resp: # noqa: S310
body = json.loads(resp.read().decode("utf-8"))
except Exception as e:
validated_ip = ipaddress.ip_address(ip)

url = f"http://ip-api.com/json/{validated_ip.compressed}"
params = {"fields": "status,lat,lon,city,country"}

response = requests.get(
url,
params=params,
timeout=4.0,
allow_redirects=False,
)
response.raise_for_status()
body = response.json()

except (ValueError, requests.RequestException) as e:
logger.warning("ip-api geolocation failed for {}: {}", ip, e)
return None

Expand Down
4 changes: 2 additions & 2 deletions opensampl/mixins/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from abc import ABC, abstractmethod
from collections.abc import Callable
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -137,7 +137,7 @@ def _collect_and_save(cls, collect_config: CollectConfig) -> None:
if collect_config.output_dir:
file_content = cls.create_file_content(data)
collect_config.output_dir.mkdir(parents=True, exist_ok=True)
now_stamp = datetime.now(tz=timezone.utc).timestamp()
now_stamp = datetime.now(tz=UTC).timestamp()
output = collect_config.output_dir / f"{cls.vendor.parser_class}_{data.probe_key!r}_{now_stamp}.txt"
output.write_text(file_content)

Expand Down
4 changes: 2 additions & 2 deletions opensampl/mixins/random_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import random
from abc import abstractmethod
from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -53,7 +53,7 @@ def _generate_random_ip(cls) -> str:
def define_start_time(self):
"""If start_time is None at the end of validation,"""
if self.start_time is None:
self.start_time = datetime.now(tz=timezone.utc) - timedelta(hours=self.duration_hours)
self.start_time = datetime.now(tz=UTC) - timedelta(hours=self.duration_hours)
return self

@field_validator("*", mode="before")
Expand Down
2 changes: 1 addition & 1 deletion opensampl/server/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FROM python:3.14 AS base
ARG OPENSAMPL_VERSION=1.2.5
ARG OPENSAMPL_VERSION=1.2.6

WORKDIR /tmp
ENV ROUTE_TO_BACKEND=false
Expand Down
6 changes: 3 additions & 3 deletions opensampl/server/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
import time
from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any

import pandas as pd
Expand Down Expand Up @@ -95,7 +95,7 @@ def get_keys():
try:
Session = sessionmaker(bind=engine) # noqa: N806
with Session() as session:
now = datetime.now(tz=timezone.utc)
now = datetime.now(tz=UTC)
stmt = select(APIAccessKey.key).where(or_(APIAccessKey.expires_at.is_(None), APIAccessKey.expires_at > now))
result = session.execute(stmt)
keys = [row[0] for row in result.all()]
Expand Down Expand Up @@ -331,7 +331,7 @@ def generate_api_key(
new_key = APIAccessKey()
new_key.generate_key()
if expire_after:
new_key.expires_at = datetime.now(tz=timezone.utc) + timedelta(days=expire_after)
new_key.expires_at = datetime.now(tz=UTC) + timedelta(days=expire_after)

session.add(new_key)

Expand Down
2 changes: 1 addition & 1 deletion opensampl/server/migrations/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.14

ARG OPENSAMPL_VERSION=1.2.5
ARG OPENSAMPL_VERSION=1.2.6

WORKDIR /
RUN pip install --no-cache-dir \
Expand Down
6 changes: 3 additions & 3 deletions opensampl/vendors/adva.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import gzip
import random
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import ClassVar, TextIO

Expand Down Expand Up @@ -90,7 +90,7 @@ def parse_file_name(cls, file_name: Path) -> tuple[ProbeKey, datetime]:
)

# Convert timestamp to datetime object
timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").astimezone(tz=timezone.utc)
timestamp = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").astimezone(tz=UTC)

return ProbeKey(probe_id=probe_id, ip_address=ip_address), timestamp
raise ValueError(f"Could not parse file name {file_name} into probe key and timestamp for ADVA probe")
Expand Down Expand Up @@ -159,7 +159,7 @@ def process_metadata(self) -> dict:
else:
freeform_header[key] = value
headers["additional_metadata"] = freeform_header
self.start_time = datetime.strptime(headers["start"], "%Y/%m/%d %H:%M:%S").astimezone(tz=timezone.utc)
self.start_time = datetime.strptime(headers["start"], "%Y/%m/%d %H:%M:%S").astimezone(tz=UTC)
self.metadata_parsed = True
return headers

Expand Down
6 changes: 3 additions & 3 deletions opensampl/vendors/base_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar

Expand Down Expand Up @@ -297,7 +297,7 @@ def archive_file(self, archive_dir: Path):
Puts the file in the archive directory, with year/month/vendor/ipaddress_id hierarchy based on
date that the file was processed.
"""
now = datetime.now(tz=timezone.utc)
now = datetime.now(tz=UTC)
archive_path = archive_dir / str(now.year) / f"{now.month:02d}" / self.vendor.name / str(self.probe_key)
archive_path.mkdir(parents=True, exist_ok=True)
shutil.move(str(self.input_file), str(archive_path / self.input_file.name))
Expand Down Expand Up @@ -447,7 +447,7 @@ def _process_directory(cls, config: LoadConfig, extra_kwargs: dict) -> None:
for future in futures:
try:
future.result()
except Exception as e: # noqa: PERF203
except Exception as e:
logger.error(f"Error in thread: {e!s}")

@property
Expand Down
Loading
Loading