Skip to content

Commit

Permalink
Merge pull request #7 from dtrai2/2-create-config-class
Browse files Browse the repository at this point in the history
create config class with automatic validation
  • Loading branch information
dtrai2 authored Jul 29, 2024
2 parents d7f2558 + 70ef299 commit ae63b81
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 67 deletions.
40 changes: 4 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,15 @@ pip install shiny-invoice
```

Once `shiny-invoice` is installed you need to create configuration file.
A suitable example looks like this:

```yaml
paths:
invoices_dir: /home/user/invoices/ # must be an absolute path, and it needs to end with /
html_template: shiny_invoice/templates/default_en.html
datastore: datastore.json
company: # here you can specify details of your company
name: Company Name
skills:
- Primary Skill
- Secondary Skill
address:
- Address line 1
- 4234 Addresline2
contact:
- [email protected]
- +49 123 456789
- shinyinvoice.de
bank:
name: SomeBank
iban: DE12 1234 5678 9100 00
bic: BICCCCCCC
tax_number: 11/2222/3333
tax_rate: 0.19
payment_terms_days: 14
invoice_defaults: # here you can set defaults, which will be used to prefill the invoice formular
introduction: Dear Sir or Madam,
recipient: |-
Comp 2
Compstreet Comp
1335 Compvill
items: |
Services, Hours, Rate, Price
Service 1, 40h, 100 €, 4.000 €
A suitable default configuration can be generated with
```bash
shiny-invoice generate-default-config
```

Once everything is set up you can run `shiny-invoice` with:

```bash
shiny-invoice run --config config.yaml
shiny-invoice run --config default_config.yaml
```

More information you can find with
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ dependencies = [
"shiny",
"click",
"ruamel.yaml",
"pandas"
"pandas",
"pydantic",
"tinydb"
]

[project.optional-dependencies]
Expand Down
50 changes: 50 additions & 0 deletions shiny_invoice/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""This module contains the definition of the configurations and the respecting default values"""

from typing import List

from pydantic import BaseModel, PositiveInt


class PathConfig(BaseModel):
"""Config values for different resources"""

invoices_dir: str = "/home/user/invoices/"
html_template: str = "shiny_invoice/templates/default_en.html"
datastore: str = "datastore.json"


class BankConfig(BaseModel):
"""Configuration of bank details"""

name: str = "Some Bank"
iban: str = "DE12 1234 5678 9100 00"
bic: str = "BICCCCCCC"
tax_number: str = "11/2222/3333"


class CompanyConfig(BaseModel):
"""Configuration of company details"""

name: str = "Company Name"
skills: List[str] = ["Primary Skill", "Secondary Skill"]
address: List[str] = ["Address line 1", "4234 Addressline 2"]
contact: List[str] = ["[email protected]", "+49 123 456789", "shinyinvoice.de"]
bank: BankConfig = BankConfig()
tax_rate: float = 0.19
payment_terms_days: PositiveInt = 14


class InvoiceDefaultsConfig(BaseModel):
"""Configuration of invoice defaults that should be rendered in the invoice"""

introduction: str = "Dear Sir or Madam,"
recipient: str = "Comp 2\nCompstreet Comp\n1335 Compvill"
items: str = "Services, Hours, Rate, Price\nService 1, 40h, 100 €, 4.000 €"


class Config(BaseModel):
"""Shiny-Invoice configuration"""

paths: PathConfig = PathConfig()
company: CompanyConfig = CompanyConfig()
invoice_defaults: InvoiceDefaultsConfig = InvoiceDefaultsConfig()
18 changes: 16 additions & 2 deletions shiny_invoice/shiny_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
as well as the cli.
"""

import sys
from pathlib import Path

import click
from pydantic import ValidationError
from ruamel.yaml import YAML
from shiny import App, Inputs, Outputs, Session, ui

from shiny_invoice.config import Config
from shiny_invoice.ui_config import config_ui, config_server
from shiny_invoice.ui_existing_invoices import existing_invoices_ui, existing_invoices_server
from shiny_invoice.ui_new_invoice import new_invoice_ui, new_invoice_server
Expand All @@ -22,6 +25,13 @@ def cli():
"""Shiny Invoice CLI"""


@cli.command(short_help="Generate default config")
def generate_default_config():
"""Generate a yaml file with the default configuration for shiny-invoice"""
with open("default_config.yaml", "w", encoding="utf8") as f:
yaml.dump(Config().model_dump(), f)


@cli.command(short_help="Run Shiny Invoice")
@click.option(
"--config",
Expand All @@ -39,7 +49,11 @@ def run(config: Path, host: str, port: int):
"""Run shiny invoice"""
with open(config, "r", encoding="utf8") as file:
config_str = file.read()
config = yaml.load(config_str)
try:
config: Config = Config(**yaml.load(config_str))
except ValidationError as e:
print(e.errors())
sys.exit(-1)

# pylint: disable=too-many-function-args
app_ui = ui.page_navbar(
Expand All @@ -60,7 +74,7 @@ def server(input: Inputs, output: Outputs, session: Session):

# pylint: enable=redefined-builtin, unused-argument, no-value-for-parameter

app = App(app_ui, server, static_assets=config.get("paths").get("invoices_dir"))
app = App(app_ui, server, static_assets=config.paths.invoices_dir)
app.run(host=host, port=port)


Expand Down
6 changes: 3 additions & 3 deletions shiny_invoice/templates/default_de.html
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,9 @@
${invoice_items}
<div class="totals">
<table>
<tr><td>Net total:</td><td>${total_net}</td></tr>
<tr><td>VAT ${tax_rate}:</td><td>${tax}</td></tr>
<tr><td>Gross total:</td><td>${total_gross}</td></tr>
<tr><td>Gesamt Netto:</td><td>${total_net}</td></tr>
<tr><td>Umsatzsteuer ${tax_rate}:</td><td>${tax}</td></tr>
<tr><td>Gesamtbetrag:</td><td>${total_gross}</td></tr>
</table>
</div>
<div class="closing">
Expand Down
6 changes: 4 additions & 2 deletions shiny_invoice/ui_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from ruamel.yaml import YAML
from shiny import module, ui, render

from shiny_invoice.config import Config

yaml = YAML(typ="rt", pure=True)


Expand All @@ -18,12 +20,12 @@ def config_ui():


@module.server
def config_server(_, __, ___, config):
def config_server(_, __, ___, config: Config):
"""Contains the shiny server for the configuration view"""

@render.text
def config_output():
"""Dump the configuration into a string and return it"""
buffer = io.BytesIO()
yaml.dump(config, buffer)
yaml.dump(config.model_dump(), buffer)
return buffer.getvalue().decode("utf8")
4 changes: 2 additions & 2 deletions shiny_invoice/ui_existing_invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def existing_invoices_ui():
def existing_invoices_server(input, _, __, config):
"""Contains the Shiny Server for existing invoices"""

datastore = TinyDB(config.get("paths").get("datastore"))
datastore = TinyDB(config.paths.datastore)

@reactive.calc
def get_filtered_invoices() -> pd.DataFrame | str:
Expand Down Expand Up @@ -136,7 +136,7 @@ def selected_invoice():
if len(selection) > 0:
selection = selection[0]
df = get_filtered_invoices().iloc[selection]["Link"]
root_dir = Path(config.get("paths").get("invoices_dir"))
root_dir = Path(config.paths.invoices_dir)
with open(root_dir / df.attrs.get("href"), "r", encoding="utf8") as file:
html = file.read()
return ui.HTML(html)
Expand Down
42 changes: 21 additions & 21 deletions shiny_invoice/ui_new_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@module.ui
def new_invoice_ui(config):
"""Defines the shiny ui for new invoices"""
invoice_defaults = config.get("invoice_defaults")
invoice_defaults = config.invoice_defaults

return ui.layout_column_wrap(
ui.card(
Expand All @@ -24,21 +24,21 @@ def new_invoice_ui(config):
ui.input_text(
id="introduction",
label="Introduction",
value=invoice_defaults.get("introduction"),
value=invoice_defaults.introduction,
width="100%",
),
ui.input_text_area(
id="recipient_address",
label="Recipient Address",
value=invoice_defaults.get("recipient"),
value=invoice_defaults.recipient,
rows=3,
width="100%",
),
ui.tooltip(
ui.input_text_area(
id="invoice_items",
label="Invoice Items",
value=invoice_defaults.get("items"),
value=invoice_defaults.items,
rows=6,
width="100%",
spellcheck=True,
Expand All @@ -57,9 +57,9 @@ def new_invoice_ui(config):
@module.server
def new_invoice_server(input, _, __, config):
"""Contains the Shiny Server for creating new invoices"""
datastore = TinyDB(config.get("paths").get("datastore"))
datastore = TinyDB(config.paths.datastore)

with open(Path(config.get("paths").get("html_template")), "r", encoding="utf8") as file:
with open(Path(config.paths.html_template), "r", encoding="utf8") as file:
html_template = Template(file.read())

@reactive.calc
Expand Down Expand Up @@ -92,7 +92,7 @@ def invoice_number_ui():

@render.ui
def due_date_ui():
payment_terms_days = config.get("company").get("payment_terms_days")
payment_terms_days = config.company.payment_terms_days
due_date = input.created_at_date() + datetime.timedelta(days=payment_terms_days)
return ui.input_date("due_date", "Due date", value=str(due_date), width="100%")

Expand Down Expand Up @@ -136,22 +136,22 @@ def download_button():
@reactive.calc
def render_invoice():
total_net = calculate_totals()
company = config.get("company")
tax = total_net * float(company.get("tax_rate"))
company = config.company
tax = total_net * float(company.tax_rate)
total_gross = total_net + tax
substitutions = {
"name": company.get("name"),
"primary_skills": " | ".join(company.get("skills")[:2]),
"all_skills": "<br/>".join(company.get("skills")),
"piped_address": " | ".join(company.get("address")),
"linebreaked_address": "<br/>".join(company.get("address")),
"primary_contact": "<br/>".join(company.get("contact")[:2]),
"bank": company.get("bank").get("name"),
"iban": company.get("bank").get("iban"),
"bic": company.get("bank").get("bic"),
"tax_number": company.get("bank").get("tax_number"),
"tax_rate": f"{float(company.get('tax_rate')) * 100}%",
"all_contact": "<br/>".join(company.get("contact")),
"name": company.name,
"primary_skills": " | ".join(company.skills[:2]),
"all_skills": "<br/>".join(company.skills),
"piped_address": " | ".join(company.address),
"linebreaked_address": "<br/>".join(company.address),
"primary_contact": "<br/>".join(company.contact[:2]),
"bank": company.bank.name,
"iban": company.bank.iban,
"bic": company.bank.bic,
"tax_number": company.bank.tax_number,
"tax_rate": f"{float(company.tax_rate) * 100}%",
"all_contact": "<br/>".join(company.contact),
"invoice_number": input.invoice_number(),
"created_at_date": input.created_at_date().strftime("%d.%m.%Y"),
"due_at_date": input.due_date().strftime("%d.%m.%Y"),
Expand Down

0 comments on commit ae63b81

Please sign in to comment.