Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
12c3eba
Template: Simplify upload template to only support file browser
simmsa Feb 7, 2026
0003360
JS: Simplify upload flow on initial upload screen
simmsa Feb 7, 2026
83bc89e
Ignore: App generated logs
simmsa Feb 8, 2026
8eaa393
Settings: Add logs to settings
simmsa Feb 8, 2026
c293941
Templates: Refactor to use nlr prefix
simmsa Feb 8, 2026
05c4227
Templates: Add logs template
simmsa Feb 8, 2026
8f228af
Templates: Add macro template for common components
simmsa Feb 8, 2026
e1772fe
Assets: Use nlr in logo filename
simmsa Feb 8, 2026
5bc2a38
Config: Cache package version
simmsa Feb 8, 2026
6f69c31
Config: Add logs folders
simmsa Feb 8, 2026
cb747c7
Routes: Leverage DRY and caching/class variables
simmsa Feb 8, 2026
9b99bd4
Service: Add function to get S3 client
simmsa Feb 8, 2026
3710f26
Services: Wire up logging/logger in upload_manager
simmsa Feb 8, 2026
d3a70e2
Services: Explicitly use UTC for all datetimes in upload_manager
simmsa Feb 8, 2026
105ed35
Service: Add log service for csv and jsonl logs
simmsa Feb 8, 2026
0dc3c7d
Route: Add logs api endpoint
simmsa Feb 8, 2026
c64b08c
Web: Add shared utils
simmsa Feb 8, 2026
8b4711b
Web: Add logs route to main routing
simmsa Feb 8, 2026
63fb7e0
Routes: Use logging in settings
simmsa Feb 8, 2026
54daa77
Upload: Refactor to use callback architecture
simmsa Feb 8, 2026
b939003
Service: Update cache service to understand hive partitions
simmsa Feb 8, 2026
d04392f
App: Add latest routes and imports
simmsa Feb 8, 2026
f95243a
JS: Refactor utils into separate files
simmsa Feb 8, 2026
4d209ae
JS: Add logs module
simmsa Feb 8, 2026
f89206b
JS: Add more dom helpers
simmsa Feb 8, 2026
41e6e4b
JS: Simplify about modal js
simmsa Feb 8, 2026
c0001b3
JS: Completely refactor analysis to make more sense to end user
simmsa Feb 8, 2026
4a52731
JS: Use dom.js selectors in logs.js
simmsa Feb 8, 2026
df78105
JS: Clean up file browser with dom.js
simmsa Feb 8, 2026
73465a2
JS: Add logs params to state
simmsa Feb 8, 2026
4af7250
JS: Refactor folder browser to improve usability
simmsa Feb 8, 2026
1f660c7
JS: Leverage new modules in settings to clean up
simmsa Feb 8, 2026
652c1ec
JS: Leverage new modules in stepper
simmsa Feb 8, 2026
eb7be12
JS: Leverage new modules in upload-*
simmsa Feb 8, 2026
50c1568
JS: Add new modules to entry point
simmsa Feb 8, 2026
4070ab4
Dev: Fix keyword typo
simmsa Feb 9, 2026
217cc95
Dev: Add pre commit hooks
simmsa Feb 9, 2026
c1837a3
Tests: Add initial python tests
simmsa Feb 9, 2026
b75fd15
Tests: Add initial js tests
simmsa Feb 9, 2026
3639946
Actions: Add testing action
simmsa Feb 9, 2026
2e77793
Actions: Run tests on pull requests into develop and main
simmsa Feb 9, 2026
214d0e0
Tests: Skip type checking for pandas
simmsa Feb 9, 2026
55cca8d
Reqs: Add missing mcap-ros2-support req
simmsa Feb 9, 2026
cd75378
Merge pull request #1 from MODAQ2/feat_improve_usability
simmsa Feb 9, 2026
b6d232b
Release: Prep v0.2.0 release
simmsa Feb 9, 2026
dbe4eee
Merge pull request #2 from MODAQ2/prep_v0.2_release
simmsa Feb 9, 2026
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
70 changes: 70 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Run Tests

on:
# Runs when commits are pushed directly to these branches
# Note: we are purposly skipping push in favor of branch protection rules
# that require PRs to be opened against these branches. This ensures that all
# changes are reviewed and pass tests before being merged. This avoids
# running tests on every push to feature branches, which can be noisy and
# inefficient. Instead, tests will only run when a PR is opened or updated
# that targets the main or develop branches.
# push:
# branches: [main, develop]
# Runs when a PR is opened/updated that wants to merge INTO these branches
pull_request:
branches: [main, develop]

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- uses: pre-commit/action@v3.0.1

python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt

- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt

- name: Lint
run: ruff check app/ tests/

- name: Type check
run: mypy app/

- name: Test
run: pytest tests/ -v

javascript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint, type check, and test
run: npm run check
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ settings.json
# SQLite cache
modaq_upload_cache.db

# Log files
logs/

# Python
__pycache__/
*.py[cod]
Expand Down
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
repos:
# Standard safety hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: [--maxkb=1000]
- id: check-merge-conflict
- id: check-yaml
- id: check-json
- id: no-commit-to-branch
args: [--branch, main]

# Comprehensive secret detection
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
15 changes: 14 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from flask import Flask

from app.config import get_settings
from app.config import get_package_version, get_settings


def create_app() -> Flask:
Expand All @@ -26,6 +26,7 @@ def inject_display_name() -> dict[str, str]:

# Register blueprints
from app.routes.files import files_bp
from app.routes.logs import logs_bp
from app.routes.main import main_bp
from app.routes.settings import settings_bp
from app.routes.upload import upload_bp
Expand All @@ -34,5 +35,17 @@ def inject_display_name() -> dict[str, str]:
app.register_blueprint(upload_bp, url_prefix="/api/upload")
app.register_blueprint(files_bp, url_prefix="/api/files")
app.register_blueprint(settings_bp, url_prefix="/api/settings")
app.register_blueprint(logs_bp, url_prefix="/api/logs")

# Log application startup
from app.services.log_service import get_log_service

log = get_log_service()
log.info(
"app",
"app_started",
f"Application started (v{get_package_version()})",
{"version": get_package_version()},
)

return app
120 changes: 52 additions & 68 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Configuration management for modaq_upload"""

import functools
import json
import os
import subprocess
Expand Down Expand Up @@ -28,26 +29,28 @@
ENV_S3_BUCKET = "MODAQ_S3_BUCKET"
ENV_DEFAULT_UPLOAD_FOLDER = "MODAQ_DEFAULT_UPLOAD_FOLDER"
ENV_DISPLAY_NAME = "MODAQ_DISPLAY_NAME"
ENV_LOG_DIRECTORY = "MODAQ_LOG_DIRECTORY"


def get_package_version() -> str:
"""Get the package version from pyproject.toml."""
@functools.cache
def _read_pyproject_field(field: str, default: str) -> str:
"""Read a field from pyproject.toml [project] section (cached)."""
try:
with open(PYPROJECT_FILE, "rb") as f:
pyproject = tomllib.load(f)
return str(pyproject.get("project", {}).get("version", "0.0.0"))
return str(pyproject.get("project", {}).get(field, default))
except Exception:
return "0.0.0"
return default


def get_package_version() -> str:
"""Get the package version from pyproject.toml."""
return _read_pyproject_field("version", "0.0.0")


def get_package_name() -> str:
"""Get the package name from pyproject.toml."""
try:
with open(PYPROJECT_FILE, "rb") as f:
pyproject = tomllib.load(f)
return str(pyproject.get("project", {}).get("name", "modaq-uploader"))
except Exception:
return "modaq-uploader"
return _read_pyproject_field("name", "modaq-uploader")


class Settings:
Expand Down Expand Up @@ -79,6 +82,7 @@ def _load_settings(self) -> None:
"s3_bucket": "",
"default_upload_folder": "",
"display_name": "MODAQ Uploader",
"log_directory": "logs",
}

# Load from settings.default.json if it exists
Expand All @@ -98,6 +102,7 @@ def _load_settings(self) -> None:
"s3_bucket": os.environ.get(ENV_S3_BUCKET),
"default_upload_folder": os.environ.get(ENV_DEFAULT_UPLOAD_FOLDER),
"display_name": os.environ.get(ENV_DISPLAY_NAME),
"log_directory": os.environ.get(ENV_LOG_DIRECTORY),
}

# Only apply non-None environment values
Expand Down Expand Up @@ -141,27 +146,36 @@ def reload(self) -> None:
@property
def aws_profile(self) -> str:
"""Get the AWS profile name."""
return str(self._settings.get("aws_profile", "default"))
return str(self._settings["aws_profile"])

@property
def aws_region(self) -> str:
"""Get the AWS region."""
return str(self._settings.get("aws_region", "us-west-2"))
return str(self._settings["aws_region"])

@property
def s3_bucket(self) -> str:
"""Get the S3 bucket name."""
return str(self._settings.get("s3_bucket", ""))
return str(self._settings["s3_bucket"])

@property
def default_upload_folder(self) -> str:
"""Get the default upload folder path."""
return str(self._settings.get("default_upload_folder", ""))
return str(self._settings["default_upload_folder"])

@property
def display_name(self) -> str:
"""Get the display name for the application."""
return str(self._settings.get("display_name", "MODAQ Uploader"))
return str(self._settings["display_name"])

@property
def log_directory(self) -> Path:
"""Get the log directory path (resolved to absolute path relative to BASE_DIR)."""
log_dir = str(self._settings["log_directory"])
path = Path(log_dir)
if not path.is_absolute():
path = BASE_DIR / path
return path


def get_settings() -> Settings:
Expand Down Expand Up @@ -229,35 +243,11 @@ def update_application(self) -> dict[str, Any]:
"modaq_toolkit": {"success": False, "output": ""},
}

try:
# Git pull
git_result = subprocess.run(
["git", "pull"],
cwd=self.base_dir,
capture_output=True,
text=True,
check=True,
)
results["git_pull"] = {
"success": True,
"output": git_result.stdout + git_result.stderr,
}

# Pip install requirements
pip_result = subprocess.run(
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
cwd=self.base_dir,
capture_output=True,
text=True,
check=True,
)
results["pip_install"] = {
"success": True,
"output": pip_result.stdout + pip_result.stderr,
}

# Update modaq_toolkit specifically (force reinstall to get latest)
modaq_result = subprocess.run(
steps: list[tuple[str, list[str]]] = [
("git_pull", ["git", "pull"]),
("pip_install", [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]),
(
"modaq_toolkit",
[
sys.executable,
"-m",
Expand All @@ -267,34 +257,28 @@ def update_application(self) -> dict[str, Any]:
"--force-reinstall",
"git+https://github.com/MODAQ2/MODAQ_toolkit.git",
],
cwd=self.base_dir,
capture_output=True,
text=True,
check=True,
)
results["modaq_toolkit"] = {
"success": True,
"output": modaq_result.stdout + modaq_result.stderr,
}

except subprocess.CalledProcessError as e:
# Record which step failed
cmd_name = " ".join(e.cmd) if isinstance(e.cmd, list) else str(e.cmd)
if "git" in cmd_name:
results["git_pull"] = {
"success": False,
"output": e.stdout + e.stderr if e.stdout else str(e),
}
elif "modaq" in cmd_name.lower() or "MODAQ" in cmd_name:
results["modaq_toolkit"] = {
"success": False,
"output": e.stdout + e.stderr if e.stdout else str(e),
),
]

for step_name, cmd in steps:
try:
result = subprocess.run(
cmd,
cwd=self.base_dir,
capture_output=True,
text=True,
check=True,
)
results[step_name] = {
"success": True,
"output": result.stdout + result.stderr,
}
else:
results["pip_install"] = {
except subprocess.CalledProcessError as e:
results[step_name] = {
"success": False,
"output": e.stdout + e.stderr if e.stdout else str(e),
}
break

return results

Expand Down
Loading