Skip to content

Commit

Permalink
Release: generate-inv v2025.02.19
Browse files Browse the repository at this point in the history
  • Loading branch information
kborovik committed Feb 19, 2025
1 parent bb7856c commit ac44d22
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 102 deletions.
17 changes: 14 additions & 3 deletions .harlequin.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
default_profile = "generate-inv"
default_profile = "linux"

[profiles.generate-inv]
[profiles.linux]
adapter = "sqlite"
conn_str = [ "/Users/kb/.local/share/generate-inv/generate-inv.db" ]
conn_str = [
"/home/kb/.local/share/generate-inv/generate-inv.db",
]
keymap_name = [ "vscode" ]
limit = 100000
theme = "nord"

[profiles.macos]
adapter = "sqlite"
conn_str = [
"/Users/kb/.local/share/generate-inv/generate-inv.db",
]
keymap_name = [ "vscode" ]
limit = 100000
theme = "nord"
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "CurrentFile",
"type": "debugpy",
"request": "launch",
"module": "gcp_iam_roles.${fileBasenameNoExtension}",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceRoot}:${PYTHONPATH}",
"VIRTUAL_ENV": "${workspaceRoot}/.venv"
},
},
{
"name": "package",
"type": "debugpy",
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"dotenv"
"dotenv",
"pytest"
]
}
50 changes: 18 additions & 32 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ MAKEFLAGS += --no-builtin-rules --no-builtin-variables

PATH := $(HOME)/.cargo/bin:$(abspath .venv)/bin:$(PATH)

LOGFIRE_TOKEN := $(file < $(HOME)/.secrets/LOGFIRE_TOKEN)
GEMINI_API_KEY := $(file < $(HOME)/.secrets/GEMINI_API_KEY)
ANTHROPIC_API_KEY := $(file < $(HOME)/.secrets/ANTHROPIC_API_KEY)

ifneq (,$(wildcard pyproject.toml))
NAME := $(shell yq -p toml -o yaml '.project.name' pyproject.toml)
MODULE := $(shell yq -p toml -o yaml '.project.scripts' pyproject.toml | cut -d':' -f2 | xargs)
Expand All @@ -22,9 +18,6 @@ settings: setup
$(call var,VERSION,$(VERSION))
$(call var,NAME,$(NAME))
$(call var,MODULE,$(MODULE))
$(call var,LOGFIRE_TOKEN,$(LOGFIRE_TOKEN))
$(call var,GEMINI_API_KEY,$(GEMINI_API_KEY))
$(call var,ANTHROPIC_API_KEY,$(ANTHROPIC_API_KEY))

help:
echo "Usage: make [recipe]"
Expand All @@ -38,11 +31,11 @@ help:
} \
}' $(MAKEFILE_LIST)

setup: $(uv_bin) .gitignore data .venv .env uv.lock
setup: $(uv_bin) .gitignore data .venv uv.lock

test: db-schema ## Run Python tests
test: ## Run Python tests
$(call header,Running Python tests)
pytest -v -m db
pytest -v

ruff-format:
ruff format .
Expand All @@ -63,8 +56,8 @@ clean: ## Reset development environment
rm -rf .venv requirements.txt build/ dist/ *.egg-info/
find . -type d -name "__pycache__" -exec rm -rf {} +

run-generate: ## Run Python generate module
uv run -m invoice_ocr.generate
run: ## Run Python application
uv run $(NAME)

uv_bin := $(HOME)/.cargo/bin/uv

Expand All @@ -81,20 +74,23 @@ $(uv_bin):
.env
EOF

define DOT_ENV
LOGFIRE_TOKEN=$(LOGFIRE_TOKEN)
GEMINI_API_KEY=$(GEMINI_API_KEY)
ANTHROPIC_API_KEY=$(ANTHROPIC_API_KEY)
endef

.env:
echo "$$DOT_ENV" > $(@)

data:
mkdir -p $(@)

.venv:
uv venv
uv sync

pyproject.toml:
uv init --package
uv add --dev ruff
uv add --dev pytest

uv.lock: pyproject.toml
uv sync && touch $(@)

requirements.txt: uv.lock
uv pip freeze --exclude-editable --color never >| $(@)

define INIT_PY
from importlib.metadata import version
Expand All @@ -109,16 +105,6 @@ src-init:
echo "$$INIT_PY" >| src/$(MODULE)/__init__.py
ruff format .

pyproject.toml:
uv init --package
uv add --dev ruff

uv.lock: pyproject.toml
uv sync --inexact && touch $(@)

requirements.txt: uv.lock
uv pip freeze --exclude-editable --color never >| $(@)

version: ## Update version
$(eval pre_release := $(shell date '+%H%M' | sed 's/^0*//'))
$(eval version := $(shell date '+%Y.%m.%d.post$(pre_release)'))
Expand All @@ -134,7 +120,7 @@ release: ## Create GitHub Release
$(eval version := $(shell date '+%Y.%m.%d'))
set -e
sed -i 's/version = "[0-9]\+\.[0-9]\+\.[0-9]\+.*"/version = "$(version)"/' pyproject.toml
uv sync --inexact
uv sync
git add --all
rm -rf dist/
uv build --wheel
Expand Down
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ dependencies = [
"jinja2>=3.1.5",
"pydantic-ai-slim[anthropic]>=0.0.23",
"python-dotenv>=1.0.1",
"sqlalchemy>=2.0.38",
"sqlmodel>=0.0.22",
"typer>=0.15.1",
"weasyprint>=64.0",
]
description = "Add your description here"
name = "generate-inv"
readme = "README.md"
requires-python = ">=3.12"
version = "2025.02.15.post2204"
version = "2025.02.19"

[project.scripts]
generate-inv = "generate_inv:cli"
Expand All @@ -25,6 +25,7 @@ requires = [ "hatchling" ]

[dependency-groups]
dev = [
"pytest>=8.3.4",
"ruff>=0.9.5",
]

Expand All @@ -37,3 +38,8 @@ select = [ "C90", "F", "N", "N", "PL", "RUF", "SIM", "UP" ]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.pytest.ini_options]
markers = [
"cli: Test command line application)",
]
Binary file added signing-key.gpg
Binary file not shown.
36 changes: 36 additions & 0 deletions src/generate_inv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""Typer CLI for generate_inv.
Cody instructions:
- Use Typer v0.15.0 and above
- Use Rich v13.0.0 and above
"""

from importlib.metadata import metadata
from pathlib import Path
from typing import Annotated
Expand Down Expand Up @@ -77,6 +84,35 @@ def settings(
raise Exit(0)


@cli.command(no_args_is_help=True)
def db(
show_schema: Annotated[
bool | None, Option("--show-schema", help="List database schema")
] = None,
drop_schema: Annotated[
bool | None, Option("--drop-schema", help="Drop database schema")
] = None,
) -> None:
"""Database operations"""
from sqlalchemy import inspect

from .db import DB_ENGINE, DbBase

if show_schema:
inspector = inspect(DB_ENGINE)
for table_name in inspector.get_table_names():
console.print(f"Table Name: {table_name}")
columns = inspector.get_columns(table_name)
console.print(columns)
console.print("Table Foreign Keys")
constraints = inspector.get_foreign_keys(table_name)
console.print(constraints)

if drop_schema:
console.print(f"Dropping database schema for {DB_ENGINE.url}")
DbBase.metadata.drop_all(DB_ENGINE)


@cli.callback(invoke_without_command=True)
def callback(
version: Annotated[bool | None, Option("--version", help="Show program version")] = None,
Expand Down
61 changes: 36 additions & 25 deletions src/generate_inv/db.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,74 @@
"""Database schema for the invoice generator.
Cody instructions:
- Use SQLAlchemy v2.0 and above
- Use sQLAlchemy ORM Unit Work pattern
- Use SQLite for the database
"""

from sqlalchemy import Float, ForeignKey, Integer, String, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

from . import DB_FILE

ENGINE = create_engine(f"sqlite:///{DB_FILE}", echo=True, echo_pool=True)
DB_ENGINE = create_engine(f"sqlite:///{DB_FILE}", echo=False)


class Base(DeclarativeBase):
class DbBase(DeclarativeBase):
pass


class Address(Base):
class DbAddress(DbBase):
"""Address table mirrors Address model in the types.py file."""

__tablename__ = "addresses"

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
address_line1: Mapped[str] = mapped_column(String(255), nullable=False)
address_line2: Mapped[str] = mapped_column(String(255), nullable=True)
city: Mapped[str] = mapped_column(String(100), nullable=False)
province: Mapped[str] = mapped_column(String(100), nullable=False)
postal_code: Mapped[str] = mapped_column(String(7), nullable=False)
country: Mapped[str] = mapped_column(String(100), nullable=False, default="Canada")
address_line1: Mapped[str] = mapped_column(String, nullable=False)
address_line2: Mapped[str] = mapped_column(String, nullable=True)
city: Mapped[str] = mapped_column(String, nullable=False)
province: Mapped[str] = mapped_column(String, nullable=False)
postal_code: Mapped[str] = mapped_column(String, nullable=False)
country: Mapped[str] = mapped_column(String, nullable=False, default="Canada")


class Company(Base):
class DbCompany(DbBase):
"""Company table mirrors Company model in the types.py file."""

__tablename__ = "companies"

company_id: Mapped[str] = mapped_column(String(5), primary_key=True)
company_name: Mapped[str] = mapped_column(String(255), nullable=False)
phone_number: Mapped[str] = mapped_column(String(20), nullable=False)
email: Mapped[str] = mapped_column(String(255), nullable=False)
website: Mapped[str] = mapped_column(String(255), nullable=False)
company_id: Mapped[str] = mapped_column(String, primary_key=True)
company_name: Mapped[str] = mapped_column(String, nullable=False)
phone_number: Mapped[str] = mapped_column(String, nullable=False)
email: Mapped[str] = mapped_column(String, nullable=False)
website: Mapped[str] = mapped_column(String, nullable=False)

# Foreign key relationships for addresses
address_billing_fk: Mapped[int] = mapped_column(ForeignKey("addresses.id"), nullable=False)
address_shipping_fk: Mapped[int] = mapped_column(ForeignKey("addresses.id"), nullable=True)

# Relationship definitions
address_billing: Mapped["Address"] = relationship("Address", foreign_keys=[address_billing_fk])
address_shipping: Mapped["Address"] = relationship(
"Address", foreign_keys=[address_shipping_fk]
address_billing: Mapped["DbAddress"] = relationship(
DbAddress, foreign_keys=[address_billing_fk]
)
address_shipping: Mapped["DbAddress"] = relationship(
DbAddress, foreign_keys=[address_shipping_fk]
)


class InvoiceItems(Base):
class DbInvoiceItems(DbBase):
"""Invoice items table mirrors InvoiceItem model in the types.py file."""

__tablename__ = "invoice_items"

item_sku: Mapped[str] = mapped_column(String(5), primary_key=True)
item_info: Mapped[str] = mapped_column(String(255), nullable=False)
quantity: Mapped[int] = mapped_column(Integer(), nullable=False)
item_sku: Mapped[str] = mapped_column(String, primary_key=True)
item_info: Mapped[str] = mapped_column(String, nullable=False)
quantity: Mapped[int] = mapped_column(Integer, nullable=False)
unit_price: Mapped[float] = mapped_column(Float(precision=2), nullable=False)
total_price: Mapped[float] = mapped_column(Float(precision=2), nullable=False)


DbBase.metadata.create_all(DB_ENGINE)


if __name__ == "__main__":
Base.metadata.drop_all(ENGINE)
Base.metadata.create_all(ENGINE)
pass
3 changes: 2 additions & 1 deletion src/generate_inv/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
from pydantic_ai.models.anthropic import AnthropicModel

from .__init__ import console
from .models import InvoiceItem
from .settings import ANTHROPIC_API_KEY, PYDANTIC_AI_MODEL
from .types import Company, Invoice, InvoiceItem
from .types import Company, Invoice

anthropic_model = AnthropicModel(
model_name=PYDANTIC_AI_MODEL,
Expand Down
48 changes: 48 additions & 0 deletions src/generate_inv/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from pydantic import model_validator
from sqlmodel import Field, SQLModel, create_engine

from . import DB_FILE

DB_ENGINE = create_engine(f"sqlite:///{DB_FILE}", echo=False)


class InvoiceItem(SQLModel, table=True):
id: int | None = Field(
default=None,
primary_key=True,
)
item_sku: str = Field(
description="Stock Keeping Unit (SKU) number, must be 4 uppercase letters followed by 1 number",
)
item_info: str = Field(
description="Item or service short information description",
)
quantity: int = Field(
description="Quantity of items",
)
unit_price: float = Field(
description="Price per unit",
)
total_price: float = Field(
description="Total price for the item",
default=0.0,
)

@model_validator(mode="after")
def calculate_total_price(self) -> "InvoiceItem":
self.total_price = self.quantity * self.unit_price
return self

@property
def unit_price_formatted(self) -> str:
return f"${self.unit_price:,.2f}"

@property
def total_price_formatted(self) -> str:
return f"${self.total_price:,.2f}"


SQLModel.metadata.create_all(DB_ENGINE)

if __name__ == "__main__":
pass
Loading

0 comments on commit ac44d22

Please # to comment.