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
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 1. Global Settings
* text=auto eol=lf

# 2. Binary Files
*.pdf binary
*.gif binary
*.png binary
*.jpg binary
*.jpeg binary
*.ico binary

# 3. Lockfiles
uv.lock binary linguist-generated
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ⚡ Star Ground (v2.2.1)
# ⚡ Star Ground (v2.3.0)

![Python Version](https://img.shields.io/badge/python-3.13-blue.svg)
[![Python Application CI](https://github.com/JacksonFergusonDev/star-ground/actions/workflows/python-app.yml/badge.svg)](https://github.com/JacksonFergusonDev/star-ground/actions/workflows/python-app.yml)
Expand Down
6 changes: 2 additions & 4 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,7 @@ def render_preset_selector(slot: ProjectSlot, idx: int):
else:
# If All sources, show all unique categories across everything
# Flatten the list of lists
flat_cats = sorted(
list(set(cat for sublist in cat_map.values() for cat in sublist))
)
flat_cats = sorted({cat for sublist in cat_map.values() for cat in sublist})
cat_options = ["All"] + flat_cats

cat_key = f"filter_cat_{slot.id}"
Expand Down Expand Up @@ -331,7 +329,7 @@ def on_method_change(slot_id: str):

# Handle specific initialization for Presets
if new_method == "Preset":
first_preset = sorted(list(BOM_PRESETS.keys()))[0]
first_preset = sorted(BOM_PRESETS.keys())[0]
preset_obj = BOM_PRESETS[first_preset]

if isinstance(preset_obj, dict):
Expand Down
30 changes: 22 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "star-ground"
version = "2.2.1"
version = "2.3.0"
description = "Logistics engine for analog electronics"
readme = "README.md"
requires-python = ">=3.13,<3.14"
Expand All @@ -13,6 +13,13 @@ dependencies = [
"streamlit==1.54.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]

[dependency-groups]
dev = [
"bump-my-version>=1.2.6",
Expand Down Expand Up @@ -42,6 +49,12 @@ dev = [
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = false
check_untyped_defs = true
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = ["tests.*", "app"]
disallow_untyped_defs = false

[tool.pytest.ini_options]
pythonpath = "."
Expand All @@ -56,7 +69,7 @@ filterwarnings = [
]

[tool.bumpversion]
current_version = "2.2.1"
current_version = "2.3.0"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
Expand All @@ -78,11 +91,6 @@ filename = "README.md"
search = "v{current_version}"
replace = "v{new_version}"

[[tool.bumpversion.files]]
filename = "uv.lock"
search = "name = \"star-ground\"\nversion = \"{current_version}\""
replace = "name = \"star-ground\"\nversion = \"{new_version}\""

[tool.ruff]
# Target Python 3.13 for code generation and linting analysis
target-version = "py313"
Expand All @@ -108,7 +116,10 @@ exclude = [
# B: flake8-bugbear (finds likely bugs and design problems)
# UP: pyupgrade (modernizes code, e.g., List[] -> list[])
# SIM: flake8-simplify (logic simplification)
select = ["E", "F", "I", "B", "UP", "SIM"]
# T20: flake8-print (Catch print() statements)
# PT: flake8-pytest-style (Best practices for tests)
# C4: flake8-comprehensions (Better list/dict comprehensions)
select = ["E", "F", "I", "B", "UP", "SIM", "T20", "PT", "C4"]

# Ignore E501 (Line too long) as ruff format handles line wrapping efficiently
ignore = ["E501"]
Expand All @@ -121,6 +132,9 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
known-first-party = ["src"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]

[tool.ruff.lint.per-file-ignores]
"tools/*.py" = ["T201", "T203"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
Expand Down
534 changes: 135 additions & 399 deletions requirements.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/bom_lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@
# Master Keyword List
# Combines all functional names and extra descriptive terms.
# Sorted list used for deterministic Regex generation in the PDF parser.
KEYWORDS = sorted(list(POT_NAMES | SWITCH_LABELS | KEYWORD_EXTRAS))
KEYWORDS = sorted(POT_NAMES | SWITCH_LABELS | KEYWORD_EXTRAS)

# Manufacturing Artifact Exclusion List
# These tokens indicate lines in a BOM that describe non-purchasable items
Expand Down
4 changes: 2 additions & 2 deletions src/bom_lib/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_preset_metadata() -> tuple[
)

return (
sorted(list(sources)),
{k: sorted(list(v)) for k, v in categories.items()},
sorted(sources),
{k: sorted(v) for k, v in categories.items()},
lookup,
)
2 changes: 1 addition & 1 deletion src/bom_lib/sourcing.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def inject(
qty_per_pedal: int,
note: str,
qty_override: int | None = None,
):
) -> None:
key = f"{category} | {val}"
total_qty = (
qty_override if qty_override is not None else (qty_per_pedal * pedal_count)
Expand Down
2 changes: 1 addition & 1 deletion src/bom_lib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class Inventory(UserDict):
invalid state transitions (e.g., assigning string to quantity).
"""

def __init__(self, data=None):
def __init__(self, data: dict[str, PartData] | None = None) -> None:
super().__init__(data)
# Ensure default factory behavior for new keys
if self.data is None:
Expand Down
8 changes: 6 additions & 2 deletions src/feedback.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import datetime
from typing import TYPE_CHECKING

import streamlit as st

if TYPE_CHECKING:
import gspread


@st.cache_resource(ttl="1h")
def get_gsheet_client():
def get_gsheet_client() -> "gspread.Client":
"""
Establishes a persistent connection to Google Sheets.

Expand All @@ -28,7 +32,7 @@ def get_gsheet_client():
return gspread.authorize(creds)


def save_feedback(rating, text):
def save_feedback(rating: str, text: str) -> None:
"""
Appends a new feedback entry to the "Star Ground Feedback" Google Sheet.

Expand Down
22 changes: 12 additions & 10 deletions src/pdf_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class StickerSheet(FPDF):
- Margins: optimized for standard US Letter.
"""

def __init__(self):
def __init__(self) -> None:
# Letter size (215.9mm x 279.4mm)
super().__init__(format="Letter", unit="mm")
self.set_auto_page_break(auto=False)
Expand All @@ -130,7 +130,9 @@ def __init__(self):

self.add_page()

def add_sticker(self, project_code: str, part_val: str, refs: list[str], qty: int):
def add_sticker(
self, project_code: str, part_val: str, refs: list[str], qty: int
) -> None:
"""
Draws a single sticker at the next available slot.

Expand Down Expand Up @@ -209,12 +211,12 @@ class FieldManual(FPDF):
- Logic to handle large checklists that span multiple pages.
"""

def __init__(self):
def __init__(self) -> None:
super().__init__()
self.set_auto_page_break(auto=True, margin=15)
self.set_title("Star Ground Field Manual")

def header(self):
def header(self) -> None:
"""Renders the header on every page."""
self.set_font("Courier", "B", 10)
self.cell(
Expand All @@ -228,17 +230,17 @@ def header(self):
self.line(10, 20, 200, 20)
self.ln(10)

def footer(self):
def footer(self) -> None:
"""Renders the footer on every page."""
self.set_y(-15)
self.set_font("Courier", "I", 8)
self.cell(0, 10, f"Page {self.page_no()}", align="C")

def draw_checkbox(self, x: float, y: float):
def draw_checkbox(self, x: float, y: float) -> None:
"""Draws a square checkbox at the specified coordinates."""
self.rect(x, y, 4, 4)

def add_project(self, project_name: str, parts: list[dict]):
def add_project(self, project_name: str, parts: list[dict]) -> None:
"""
Adds a full project checklist to the PDF.

Expand Down Expand Up @@ -367,7 +369,7 @@ def sort_by_z_height(part_list: list[dict]) -> list[dict]:
"ICs": 90, # "Last" (Chip Insertion)
}

def get_rank(item):
def get_rank(item: dict) -> int:
cat = item["category"]
val = str(item["value"])

Expand Down Expand Up @@ -409,7 +411,7 @@ def float_val_check(val_str: str) -> float:

def _write_field_manuals(
zf: zipfile.ZipFile, inventory: Inventory, slots: list[ProjectSlot]
):
) -> None:
"""Helper: Generates Field Manual PDFs and writes them to the ZIP archive."""
processed_projects = set()

Expand Down Expand Up @@ -466,7 +468,7 @@ def _write_field_manuals(

def _write_stickers(
zf: zipfile.ZipFile, inventory: Inventory, slots: list[ProjectSlot]
):
) -> None:
"""Helper: Generates Sticker Sheet PDFs and writes them to the ZIP archive."""
processed_projects = set()

Expand Down
Empty file added tests/__init__.py
Empty file.
4 changes: 2 additions & 2 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,8 +601,8 @@ def test_preset_integrity():
3. The parser can successfully find parts in it.
"""
for name, data in BOM_PRESETS.items():
# Handle new Dict format vs Legacy string
raw_text = data["bom_text"] if isinstance(data, dict) else data
val = data["bom_text"] if isinstance(data, dict) else data
raw_text = str(val)

# Sanity check: Text should exist
assert raw_text.strip(), f"Preset '{name}' is empty!"
Expand Down
4 changes: 2 additions & 2 deletions tools/generate_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
OUTPUT_FILE = "src/bom_lib/_presets_data.py"


def main():
def main() -> None:
"""
Main execution entry point.

Expand Down Expand Up @@ -135,7 +135,7 @@ def main():
data = presets[k]
# Manual formatting to ensure BOM text uses Python triple quotes correctly.
# We indent deeply (12 spaces) to align inside the dict structure.
content = data["bom_text"].strip().replace("\n", "\n ")
content = str(data["bom_text"]).strip().replace("\n", "\n ")

f.write(f" {repr(k)}: {{\n")
f.write(f' \'bom_text\': """\n {content}\n """,\n')
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.