Skip to content

Commit 7669664

Browse files
committed
checkpoint: model validation approaches tested
1 parent 66e6c2f commit 7669664

File tree

6 files changed

+156
-77
lines changed

6 files changed

+156
-77
lines changed

app/demo.py

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,70 @@
1-
from typing import List, Optional, Callable
1+
from typing import Callable, List, Optional
22

3+
import datetime
34
import random
5+
from datetime import date, timedelta
46
from pathlib import Path
5-
from tuttle.calendar import Calendar, ICSCalendar
7+
from decimal import Decimal
8+
69
import faker
7-
import random
8-
import datetime
9-
from datetime import timedelta, date
1010
import ics
11-
from sqlmodel import Field, SQLModel, create_engine, Session, select
11+
import numpy
1212
import sqlalchemy
1313
from loguru import logger
14-
import numpy
14+
from sqlmodel import Field, Session, SQLModel, create_engine, select
1515

16+
from tuttle import rendering
17+
from tuttle.calendar import Calendar, ICSCalendar
1618
from tuttle.model import (
1719
Address,
18-
Contact,
20+
BankAccount,
1921
Client,
20-
Project,
22+
Contact,
2123
Contract,
22-
TimeUnit,
2324
Cycle,
24-
User,
25-
BankAccount,
2625
Invoice,
2726
InvoiceItem,
27+
Project,
28+
TimeUnit,
29+
User,
2830
)
29-
from tuttle import rendering
3031

3132

3233
def create_fake_contact(
3334
fake: faker.Faker,
3435
):
35-
try:
36-
street_line, city_line = fake.address().splitlines()
37-
a = Address(
38-
id=id,
39-
street=street_line.split(" ")[0],
40-
number=street_line.split(" ")[1],
41-
city=city_line.split(" ")[1],
42-
postal_code=city_line.split(" ")[0],
43-
country=fake.country(),
44-
)
45-
first_name, last_name = fake.name().split(" ", 1)
46-
contact = Contact(
47-
id=id,
48-
first_name=first_name,
49-
last_name=last_name,
50-
email=fake.email(),
51-
company=fake.company(),
52-
address_id=a.id,
53-
address=a,
54-
)
55-
return contact
56-
except Exception as ex:
57-
logger.error(ex)
58-
logger.error(f"Failed to create fake contact, trying again")
59-
return create_fake_contact(fake)
36+
37+
split_address_lines = fake.address().splitlines()
38+
street_line = split_address_lines[0]
39+
city_line = split_address_lines[1]
40+
a = Address(
41+
street=street_line,
42+
number=city_line,
43+
city=city_line.split(" ")[1],
44+
postal_code=city_line.split(" ")[0],
45+
country=fake.country(),
46+
)
47+
first_name, last_name = fake.name().split(" ", 1)
48+
contact = Contact(
49+
first_name=first_name,
50+
last_name=last_name,
51+
email=fake.email(),
52+
company=fake.company(),
53+
address_id=a.id,
54+
address=a,
55+
)
56+
return contact
6057

6158

6259
def create_fake_client(
6360
invoicing_contact: Contact,
6461
fake: faker.Faker,
6562
):
6663
client = Client(
67-
id=id,
6864
name=fake.company(),
6965
invoicing_contact=invoicing_contact,
7066
)
67+
assert client.invoicing_contact is not None
7168
return client
7269

7370

@@ -92,7 +89,7 @@ def create_fake_contract(
9289
start_date=fake.date_this_year(after_today=True),
9390
rate=rate,
9491
currency="EUR", # TODO: Use actual currency
95-
VAT_rate=round(random.uniform(0.05, 0.2), 2),
92+
VAT_rate=Decimal(round(random.uniform(0.05, 0.2), 2)),
9693
unit=unit,
9794
units_per_workday=random.randint(1, 12),
9895
volume=fake.random_int(1, 1000),
@@ -147,7 +144,7 @@ def create_fake_invoice(
147144
"""
148145
invoice_number = next(invoice_number_counter)
149146
invoice = Invoice(
150-
number=invoice_number,
147+
number=str(invoice_number),
151148
date=datetime.date.today(),
152149
sent=fake.pybool(),
153150
paid=fake.pybool(),
@@ -159,6 +156,7 @@ def create_fake_invoice(
159156
number_of_items = fake.random_int(min=1, max=5)
160157
for _ in range(number_of_items):
161158
unit = fake.random_element(elements=("hours", "days"))
159+
unit_price = 0
162160
if unit == "hours":
163161
unit_price = abs(round(numpy.random.normal(50, 20), 2))
164162
elif unit == "days":
@@ -169,12 +167,11 @@ def create_fake_invoice(
169167
end_date=fake.date_this_decade(),
170168
quantity=fake.random_int(min=1, max=10),
171169
unit=unit,
172-
unit_price=unit_price,
170+
unit_price=Decimal(unit_price),
173171
description=fake.sentence(),
174-
VAT_rate=vat_rate,
172+
VAT_rate=Decimal(vat_rate),
175173
invoice=invoice,
176174
)
177-
assert invoice_item.invoice == invoice
178175

179176
try:
180177
rendering.render_invoice(
@@ -231,7 +228,6 @@ def create_demo_user() -> User:
231228
phone_number="+55555555555",
232229
VAT_number="27B-6",
233230
address=Address(
234-
name="Harry Tuttle",
235231
street="Main Street",
236232
number="450",
237233
city="Somewhere",
@@ -248,6 +244,14 @@ def create_demo_user() -> User:
248244

249245

250246
def create_fake_calendar(project_list: List[Project]) -> ics.Calendar:
247+
def random_datetime(start, end):
248+
return start + timedelta(
249+
seconds=random.randint(0, int((end - start).total_seconds()))
250+
)
251+
252+
def random_duration():
253+
return timedelta(hours=random.randint(1, 8))
254+
251255
# create a new calendar
252256
calendar = ics.Calendar()
253257

@@ -262,7 +266,7 @@ def create_fake_calendar(project_list: List[Project]) -> ics.Calendar:
262266
for _ in range(random.randint(1, 5)):
263267
# create a new event
264268
event = ics.Event()
265-
event.name = f"Meeting for #{project.tag}"
269+
event.name = f"Meeting for {project.tag}"
266270

267271
# set the event's begin and end datetime
268272
event.begin = random_datetime(month_ago, now)
@@ -273,16 +277,6 @@ def create_fake_calendar(project_list: List[Project]) -> ics.Calendar:
273277
return calendar
274278

275279

276-
def random_datetime(start, end):
277-
return start + timedelta(
278-
seconds=random.randint(0, int((end - start).total_seconds()))
279-
)
280-
281-
282-
def random_duration():
283-
return timedelta(hours=random.randint(1, 8))
284-
285-
286280
def install_demo_data(
287281
n_projects: int,
288282
db_path: str,
@@ -336,7 +330,3 @@ def install_demo_data(
336330
for project in projects:
337331
session.add(project)
338332
session.commit()
339-
340-
341-
if __name__ == "__main__":
342-
install_demo_data(n_projects=10)

app/projects/view.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def build(self):
6262
),
6363
title=views.StdBodyText(self.project.title),
6464
subtitle=views.StdBodyText(
65-
f"#{self.project.tag}",
65+
f"{self.project.tag}",
6666
color=colors.GRAY_COLOR,
6767
weight=FontWeight.BOLD,
6868
),

tuttle/calendar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
def extract_hashtag(string) -> str:
2323
"""Extract the first hashtag from a string."""
24-
match = re.search(r"#(\S+)", string)
24+
match = re.search(r"(#\S+)", string)
2525
if match:
2626
return match.group(1)
2727
else:

tuttle/model.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818

1919
# from pydantic import str
2020
from pydantic import BaseModel, condecimal, constr, validator
21-
from sqlmodel import Field, Relationship, SQLModel, Constraint
21+
from sqlmodel import SQLModel, Field, Relationship, Constraint
22+
2223

2324
from .dev import deprecated
2425
from .time import Cycle, TimeUnit
2526

2627

27-
def help(model_class):
28+
def help(model_class: Type[BaseModel]):
2829
return pandas.DataFrame(
2930
(
3031
(field_name, field.field_info.description)
@@ -125,7 +126,7 @@ class User(SQLModel, table=True):
125126
back_populates="users",
126127
sa_relationship_kwargs={"lazy": "subquery"},
127128
)
128-
VAT_number: str = Field(
129+
VAT_number: Optional[str] = Field(
129130
description="Value Added Tax number of the user, legally required for invoices.",
130131
)
131132
# User 1:1* ICloudAccount
@@ -146,7 +147,7 @@ class User(SQLModel, table=True):
146147
sa_relationship_kwargs={"lazy": "subquery"},
147148
)
148149
# TODO: path to logo image
149-
logo: Optional[str]
150+
# logo: Optional[str] = Field(default=None)
150151

151152
@property
152153
def bank_account_not_set(self) -> bool:
@@ -207,6 +208,14 @@ class Contact(SQLModel, table=True):
207208
)
208209
# post address
209210

211+
# VALIDATORS
212+
@validator("email")
213+
def email_validator(cls, v):
214+
"""Validate email address format."""
215+
if not re.match(r"[^@]+@[^@]+\.[^@]+", v):
216+
raise ValueError("Not a valid email address")
217+
return v
218+
210219
@property
211220
def name(self):
212221
if self.first_name and self.last_name:
@@ -248,7 +257,9 @@ class Client(SQLModel, table=True):
248257
"""A client the freelancer has contracted with."""
249258

250259
id: Optional[int] = Field(default=None, primary_key=True)
251-
name: str = Field(default="")
260+
name: str = Field(
261+
description="Name of the client.",
262+
)
252263
# Client 1:1 invoicing Contact
253264
invoicing_contact_id: int = Field(default=None, foreign_key="contact.id")
254265
invoicing_contact: Contact = Relationship(
@@ -361,10 +372,11 @@ class Project(SQLModel, table=True):
361372

362373
id: Optional[int] = Field(default=None, primary_key=True)
363374
title: str = Field(
364-
description="A short, unique title", sa_column_kwargs={"unique": True}
375+
description="A short, unique title",
376+
sa_column_kwargs={"unique": True},
365377
)
366378
description: str = Field(
367-
description="A longer description of the project", default=""
379+
description="A longer description of the project",
368380
)
369381
tag: str = Field(
370382
description="A unique tag, starting with a # symbol",

tuttle/rendering.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ def render_invoice(
118118
user: User,
119119
invoice: Invoice,
120120
document_format: str = "pdf",
121-
out_dir: str = None,
121+
out_dir=None,
122122
style: str = "anvil",
123123
only_final: bool = False,
124-
) -> str:
124+
):
125125
"""Render an Invoice using an HTML template.
126126
127127
Args:

0 commit comments

Comments
 (0)