f3-data-models 0.1.6__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {f3_data_models-0.1.6 → f3_data_models-0.1.8}/PKG-INFO +17 -2
- {f3_data_models-0.1.6 → f3_data_models-0.1.8}/README.md +16 -1
- {f3_data_models-0.1.6 → f3_data_models-0.1.8}/f3_data_models/models.py +155 -71
- {f3_data_models-0.1.6 → f3_data_models-0.1.8}/f3_data_models/utils.py +30 -20
- {f3_data_models-0.1.6 → f3_data_models-0.1.8}/pyproject.toml +1 -1
- {f3_data_models-0.1.6 → f3_data_models-0.1.8}/f3_data_models/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: f3-data-models
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.8
|
4
4
|
Summary: The data schema and models for F3 Nation applications.
|
5
5
|
Home-page: https://github.com/F3-Nation/f3-data-models
|
6
6
|
License: MIT
|
@@ -56,11 +56,26 @@ If you would like to make a change, you will need to:
|
|
56
56
|
source .env && alembic revision --autogenerate -m "Your Message Here"
|
57
57
|
```
|
58
58
|
3. Make any edits to the migration script in `alembic/versions`
|
59
|
-
4.
|
59
|
+
4. Run the upgrade on your local db:
|
60
|
+
```sh
|
61
|
+
source .env && alembic upgrade head
|
62
|
+
```
|
63
|
+
5. Bump the version on `pyproject.toml`:
|
64
|
+
```sh
|
65
|
+
poetry version patch[minor][major]
|
66
|
+
```
|
67
|
+
6. Tag your final commit and make sure to push those tags to trigger the pypi package build:
|
68
|
+
```sh
|
69
|
+
git tag <new_version> -a -m "Your message here"
|
70
|
+
git push origin --tags
|
71
|
+
```
|
72
|
+
> [!NOTE] The github pages documentation will be updated when you push to `main`, but if you would like to preview locally, run:
|
73
|
+
|
60
74
|
```sh
|
61
75
|
poetry run sphinx-build -b html docs docs/_build/html
|
62
76
|
cd docs
|
63
77
|
poetry run python -m http.server --directory _build/html
|
64
78
|
```
|
79
|
+
|
65
80
|
> [!TIP]
|
66
81
|
> Adding new fields as nullable (ie `Optional[]`) has the best chance of reducing breaking changes to the apps.
|
@@ -27,11 +27,26 @@ If you would like to make a change, you will need to:
|
|
27
27
|
source .env && alembic revision --autogenerate -m "Your Message Here"
|
28
28
|
```
|
29
29
|
3. Make any edits to the migration script in `alembic/versions`
|
30
|
-
4.
|
30
|
+
4. Run the upgrade on your local db:
|
31
|
+
```sh
|
32
|
+
source .env && alembic upgrade head
|
33
|
+
```
|
34
|
+
5. Bump the version on `pyproject.toml`:
|
35
|
+
```sh
|
36
|
+
poetry version patch[minor][major]
|
37
|
+
```
|
38
|
+
6. Tag your final commit and make sure to push those tags to trigger the pypi package build:
|
39
|
+
```sh
|
40
|
+
git tag <new_version> -a -m "Your message here"
|
41
|
+
git push origin --tags
|
42
|
+
```
|
43
|
+
> [!NOTE] The github pages documentation will be updated when you push to `main`, but if you would like to preview locally, run:
|
44
|
+
|
31
45
|
```sh
|
32
46
|
poetry run sphinx-build -b html docs docs/_build/html
|
33
47
|
cd docs
|
34
48
|
poetry run python -m http.server --directory _build/html
|
35
49
|
```
|
50
|
+
|
36
51
|
> [!TIP]
|
37
52
|
> Adding new fields as nullable (ie `Optional[]`) has the best chance of reducing breaking changes to the apps.
|
@@ -1,14 +1,16 @@
|
|
1
1
|
from datetime import datetime, date, time
|
2
|
-
from typing import Any, Dict, Optional
|
2
|
+
from typing import Any, Dict, List, Optional
|
3
3
|
from sqlalchemy import (
|
4
4
|
JSON,
|
5
5
|
TEXT,
|
6
6
|
TIME,
|
7
7
|
VARCHAR,
|
8
8
|
Boolean,
|
9
|
+
Column,
|
9
10
|
DateTime,
|
10
11
|
ForeignKey,
|
11
12
|
Integer,
|
13
|
+
Table,
|
12
14
|
func,
|
13
15
|
UniqueConstraint,
|
14
16
|
)
|
@@ -17,6 +19,7 @@ from sqlalchemy.orm import (
|
|
17
19
|
DeclarativeBase,
|
18
20
|
mapped_column,
|
19
21
|
Mapped,
|
22
|
+
relationship,
|
20
23
|
)
|
21
24
|
|
22
25
|
# Custom Annotations
|
@@ -256,6 +259,30 @@ class Role_x_User_x_Org(Base):
|
|
256
259
|
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
|
257
260
|
|
258
261
|
|
262
|
+
event_type_x_org_table = Table(
|
263
|
+
"event_types_x_org",
|
264
|
+
Base.metadata,
|
265
|
+
Column("event_type_id", Integer, ForeignKey("event_types.id"), primary_key=True),
|
266
|
+
Column("org_id", Integer, ForeignKey("orgs.id"), primary_key=True),
|
267
|
+
Column("is_default", Boolean, default=False, nullable=False),
|
268
|
+
)
|
269
|
+
|
270
|
+
event_tag_x_org_table = Table(
|
271
|
+
"event_tags_x_org",
|
272
|
+
Base.metadata,
|
273
|
+
Column("event_tag_id", Integer, ForeignKey("event_tags.id"), primary_key=True),
|
274
|
+
Column("org_id", Integer, ForeignKey("orgs.id"), primary_key=True),
|
275
|
+
Column("color_override", VARCHAR),
|
276
|
+
)
|
277
|
+
|
278
|
+
achievement_x_org_table = Table(
|
279
|
+
"achievements_x_org",
|
280
|
+
Base.metadata,
|
281
|
+
Column("achievement_id", Integer, ForeignKey("achievements.id"), primary_key=True),
|
282
|
+
Column("org_id", Integer, ForeignKey("orgs.id"), primary_key=True),
|
283
|
+
)
|
284
|
+
|
285
|
+
|
259
286
|
class Org(Base):
|
260
287
|
"""
|
261
288
|
Model representing an organization. The same model is used for all levels of organization (AOs, Regions, etc.).
|
@@ -282,7 +309,7 @@ class Org(Base):
|
|
282
309
|
|
283
310
|
__tablename__ = "orgs"
|
284
311
|
|
285
|
-
id: Mapped[
|
312
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
286
313
|
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("orgs.id"))
|
287
314
|
org_type_id: Mapped[int] = mapped_column(ForeignKey("org_types.id"))
|
288
315
|
default_location_id: Mapped[Optional[int]]
|
@@ -300,6 +327,19 @@ class Org(Base):
|
|
300
327
|
created: Mapped[dt_create]
|
301
328
|
updated: Mapped[dt_update]
|
302
329
|
|
330
|
+
event_types: Mapped[Optional[List["EventType"]]] = relationship(
|
331
|
+
"EventType", secondary=event_type_x_org_table, cascade="expunge"
|
332
|
+
)
|
333
|
+
event_tags: Mapped[Optional[List["EventTag"]]] = relationship(
|
334
|
+
"EventTag", secondary=event_tag_x_org_table, cascade="expunge"
|
335
|
+
)
|
336
|
+
achievements: Mapped[Optional[List["Achievement"]]] = relationship(
|
337
|
+
"Achievement", secondary=achievement_x_org_table, cascade="expunge"
|
338
|
+
)
|
339
|
+
parent_org: Mapped[Optional["Org"]] = relationship(
|
340
|
+
"Org", remote_side=[id], cascade="expunge"
|
341
|
+
)
|
342
|
+
|
303
343
|
|
304
344
|
class EventType(Base):
|
305
345
|
"""
|
@@ -309,7 +349,7 @@ class EventType(Base):
|
|
309
349
|
id (int): Primary Key of the model.
|
310
350
|
name (str): The name of the event type.
|
311
351
|
description (Optional[text]): A description of the event type.
|
312
|
-
|
352
|
+
acronym (Optional[str]): Acronyms associated with the event type.
|
313
353
|
category_id (int): The ID of the associated event category.
|
314
354
|
created (datetime): The timestamp when the record was created.
|
315
355
|
updated (datetime): The timestamp when the record was last updated.
|
@@ -320,46 +360,46 @@ class EventType(Base):
|
|
320
360
|
id: Mapped[intpk]
|
321
361
|
name: Mapped[str]
|
322
362
|
description: Mapped[Optional[text]]
|
323
|
-
|
363
|
+
acronym: Mapped[Optional[str]]
|
324
364
|
category_id: Mapped[int] = mapped_column(ForeignKey("event_categories.id"))
|
325
365
|
created: Mapped[dt_create]
|
326
366
|
updated: Mapped[dt_update]
|
327
367
|
|
328
368
|
|
329
|
-
class EventType_x_Event(Base):
|
330
|
-
|
331
|
-
|
369
|
+
# class EventType_x_Event(Base):
|
370
|
+
# """
|
371
|
+
# Model representing the association between events and event types. The intention is that a single event can be associated with multiple event types.
|
332
372
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
373
|
+
# Attributes:
|
374
|
+
# event_id (int): The ID of the associated event.
|
375
|
+
# event_type_id (int): The ID of the associated event type.
|
376
|
+
# """
|
337
377
|
|
338
|
-
|
378
|
+
# __tablename__ = "events_x_event_types"
|
339
379
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
380
|
+
# event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), primary_key=True)
|
381
|
+
# event_type_id: Mapped[int] = mapped_column(
|
382
|
+
# ForeignKey("event_types.id"), primary_key=True
|
383
|
+
# )
|
344
384
|
|
345
385
|
|
346
|
-
class EventType_x_Org(Base):
|
347
|
-
|
348
|
-
|
386
|
+
# class EventType_x_Org(Base):
|
387
|
+
# """
|
388
|
+
# Model representing the association between event types and organizations. This controls which event types are available for selection at the region level, as well as default types for each AO.
|
349
389
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
390
|
+
# Attributes:
|
391
|
+
# event_type_id (int): The ID of the associated event type.
|
392
|
+
# org_id (int): The ID of the associated organization.
|
393
|
+
# is_default (bool): Whether this is the default event type for the organization. Default is False.
|
394
|
+
# """
|
355
395
|
|
356
|
-
|
396
|
+
# __tablename__ = "event_types_x_org"
|
357
397
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
398
|
+
# event_type_id: Mapped[int] = mapped_column(
|
399
|
+
# ForeignKey("event_types.id"), primary_key=True
|
400
|
+
# )
|
401
|
+
# org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
|
402
|
+
# is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
363
403
|
|
364
404
|
|
365
405
|
class EventTag(Base):
|
@@ -385,43 +425,43 @@ class EventTag(Base):
|
|
385
425
|
updated: Mapped[dt_update]
|
386
426
|
|
387
427
|
|
388
|
-
class EventTag_x_Event(Base):
|
389
|
-
|
390
|
-
|
428
|
+
# class EventTag_x_Event(Base):
|
429
|
+
# """
|
430
|
+
# Model representing the association between event tags and events. The intention is that a single event can be associated with multiple event tags.
|
391
431
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
432
|
+
# Attributes:
|
433
|
+
# event_id (int): The ID of the associated event.
|
434
|
+
# event_tag_id (int): The ID of the associated event tag.
|
435
|
+
# """
|
396
436
|
|
397
|
-
|
437
|
+
# __tablename__ = "event_tags_x_events"
|
398
438
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
439
|
+
# event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), primary_key=True)
|
440
|
+
# event_tag_id: Mapped[int] = mapped_column(
|
441
|
+
# ForeignKey("event_tags.id"), primary_key=True
|
442
|
+
# )
|
403
443
|
|
404
444
|
|
405
|
-
class EventTag_x_Org(Base):
|
406
|
-
|
407
|
-
|
445
|
+
# class EventTag_x_Org(Base):
|
446
|
+
# """
|
447
|
+
# Model representing the association between event tags and organizations. Controls which event tags are available for selection at the region level.
|
408
448
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
449
|
+
# Attributes:
|
450
|
+
# event_tag_id (int): The ID of the associated event tag.
|
451
|
+
# org_id (int): The ID of the associated organization.
|
452
|
+
# color_override (Optional[str]): The calendar color override for the event tag (if the region wants to use something other than the default).
|
453
|
+
# """
|
414
454
|
|
415
|
-
|
455
|
+
# __tablename__ = "event_tags_x_org"
|
416
456
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
457
|
+
# event_tag_id: Mapped[int] = mapped_column(
|
458
|
+
# ForeignKey("event_tags.id"), primary_key=True
|
459
|
+
# )
|
460
|
+
# org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
|
461
|
+
# color_override: Mapped[Optional[str]]
|
422
462
|
|
423
463
|
|
424
|
-
class
|
464
|
+
class Org_x_SlackSpace(Base):
|
425
465
|
"""
|
426
466
|
Model representing the association between organizations and Slack workspaces. This is currently meant to be one to one, but theoretically could support multiple workspaces per organization.
|
427
467
|
|
@@ -430,10 +470,10 @@ class Org_x_Slack(Base):
|
|
430
470
|
slack_space_id (str): The ID of the associated Slack workspace.
|
431
471
|
"""
|
432
472
|
|
433
|
-
__tablename__ = "
|
473
|
+
__tablename__ = "orgs_x_slack_spaces"
|
434
474
|
|
435
475
|
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
|
436
|
-
slack_space_id: Mapped[
|
476
|
+
slack_space_id: Mapped[int] = mapped_column(
|
437
477
|
ForeignKey("slack_spaces.id"), primary_key=True
|
438
478
|
)
|
439
479
|
|
@@ -479,6 +519,21 @@ class Location(Base):
|
|
479
519
|
updated: Mapped[dt_update]
|
480
520
|
|
481
521
|
|
522
|
+
event_x_event_type_table = Table(
|
523
|
+
"events_x_event_types",
|
524
|
+
Base.metadata,
|
525
|
+
Column("event_id", Integer, ForeignKey("events.id"), primary_key=True),
|
526
|
+
Column("event_type_id", Integer, ForeignKey("event_types.id"), primary_key=True),
|
527
|
+
)
|
528
|
+
|
529
|
+
event_x_event_tag_table = Table(
|
530
|
+
"event_tags_x_events",
|
531
|
+
Base.metadata,
|
532
|
+
Column("event_id", Integer, ForeignKey("events.id"), primary_key=True),
|
533
|
+
Column("event_tag_id", Integer, ForeignKey("event_tags.id"), primary_key=True),
|
534
|
+
)
|
535
|
+
|
536
|
+
|
482
537
|
class Event(Base):
|
483
538
|
"""
|
484
539
|
Model representing an event or series; the same model is used for both with a self-referential relationship for series.
|
@@ -545,6 +600,15 @@ class Event(Base):
|
|
545
600
|
created: Mapped[dt_create]
|
546
601
|
updated: Mapped[dt_update]
|
547
602
|
|
603
|
+
org: Mapped[Org] = relationship(innerjoin=True, cascade="expunge")
|
604
|
+
location: Mapped[Location] = relationship(innerjoin=True, cascade="expunge")
|
605
|
+
event_types: Mapped[List[EventType]] = relationship(
|
606
|
+
secondary=event_x_event_type_table, innerjoin=True, cascade="expunge"
|
607
|
+
)
|
608
|
+
event_tags: Mapped[Optional[List[EventTag]]] = relationship(
|
609
|
+
secondary=event_x_event_tag_table, cascade="expunge"
|
610
|
+
)
|
611
|
+
|
548
612
|
|
549
613
|
class AttendanceType(Base):
|
550
614
|
"""
|
@@ -635,6 +699,9 @@ class User(Base):
|
|
635
699
|
last_name: Mapped[Optional[str]]
|
636
700
|
email: Mapped[str] = mapped_column(VARCHAR, unique=True)
|
637
701
|
phone: Mapped[Optional[str]]
|
702
|
+
emergency_contact: Mapped[Optional[str]]
|
703
|
+
emergency_phone: Mapped[Optional[str]]
|
704
|
+
emergency_notes: Mapped[Optional[str]]
|
638
705
|
home_region_id: Mapped[Optional[int]] = mapped_column(ForeignKey("orgs.id"))
|
639
706
|
avatar_url: Mapped[Optional[str]]
|
640
707
|
meta: Mapped[Optional[Dict[str, Any]]]
|
@@ -735,21 +802,21 @@ class Achievement_x_User(Base):
|
|
735
802
|
)
|
736
803
|
|
737
804
|
|
738
|
-
class Achievement_x_Org(Base):
|
739
|
-
|
740
|
-
|
805
|
+
# class Achievement_x_Org(Base):
|
806
|
+
# """
|
807
|
+
# Model representing the association between achievements and organizations.
|
741
808
|
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
809
|
+
# Attributes:
|
810
|
+
# achievement_id (int): The ID of the associated achievement.
|
811
|
+
# org_id (int): The ID of the associated organization.
|
812
|
+
# """
|
746
813
|
|
747
|
-
|
814
|
+
# __tablename__ = "achievements_x_org"
|
748
815
|
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
816
|
+
# achievement_id: Mapped[int] = mapped_column(
|
817
|
+
# ForeignKey("achievements.id"), primary_key=True
|
818
|
+
# )
|
819
|
+
# org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
|
753
820
|
|
754
821
|
|
755
822
|
class Position(Base):
|
@@ -891,3 +958,20 @@ class MagicLinkAuthSession(Base):
|
|
891
958
|
session_token: Mapped[str]
|
892
959
|
created: Mapped[dt_create]
|
893
960
|
expiration: Mapped[dt_create]
|
961
|
+
|
962
|
+
|
963
|
+
# class Org_x_SlackChannel(Base):
|
964
|
+
# """
|
965
|
+
# Model representing the association between organizations (specifically AOs) and Slack channels.
|
966
|
+
|
967
|
+
# Attributes:
|
968
|
+
# org_id (int): The ID of the associated organization.
|
969
|
+
# slack_channel_id (str): The Slack-internal ID of the associated Slack channel.
|
970
|
+
# """
|
971
|
+
|
972
|
+
# __tablename__ = "orgs_x_slack_channels"
|
973
|
+
|
974
|
+
# org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
|
975
|
+
# slack_channel_id: Mapped[str] = mapped_column(
|
976
|
+
# primary_key=True
|
977
|
+
# ) # Do we need a slack channel table?
|
@@ -3,11 +3,11 @@ from dataclasses import dataclass
|
|
3
3
|
from typing import List, Optional, Tuple, TypeVar
|
4
4
|
|
5
5
|
import sqlalchemy
|
6
|
-
from sqlalchemy import and_, select
|
6
|
+
from sqlalchemy import Select, and_, select
|
7
7
|
|
8
8
|
from sqlalchemy.dialects.postgresql import insert
|
9
9
|
from sqlalchemy.engine import Engine
|
10
|
-
from sqlalchemy.orm import sessionmaker
|
10
|
+
from sqlalchemy.orm import sessionmaker, joinedload
|
11
11
|
|
12
12
|
from f3_data_models.models import Base
|
13
13
|
|
@@ -76,30 +76,35 @@ def close_session(session):
|
|
76
76
|
T = TypeVar("T")
|
77
77
|
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
finally:
|
88
|
-
session.rollback()
|
89
|
-
close_session(session)
|
79
|
+
def _joinedloads(cls: T, query: Select, joinedloads: list | str) -> Select:
|
80
|
+
if joinedloads == "all":
|
81
|
+
joinedloads = [
|
82
|
+
getattr(cls, relationship.key)
|
83
|
+
for relationship in cls.__mapper__.relationships
|
84
|
+
]
|
85
|
+
return query.options(*[joinedload(load) for load in joinedloads])
|
86
|
+
|
90
87
|
|
91
|
-
|
88
|
+
class DbManager:
|
89
|
+
def get(cls: T, id: int, joinedloads: list | str = []) -> T:
|
92
90
|
session = get_session()
|
93
91
|
try:
|
94
|
-
|
92
|
+
query = select(cls).filter(cls.id == id)
|
93
|
+
query = _joinedloads(cls, query, joinedloads)
|
94
|
+
return session.scalars(query).unique().one()
|
95
95
|
finally:
|
96
96
|
session.rollback()
|
97
97
|
close_session(session)
|
98
98
|
|
99
|
-
def find_records(
|
100
|
-
|
99
|
+
def find_records(
|
100
|
+
cls: T, filters: Optional[List], joinedloads: List | str = []
|
101
|
+
) -> List[T]:
|
102
|
+
session = get_session(echo=True)
|
101
103
|
try:
|
102
|
-
|
104
|
+
query = select(cls)
|
105
|
+
query = _joinedloads(cls, query, joinedloads)
|
106
|
+
query = query.filter(*filters)
|
107
|
+
records = session.scalars(query).unique().all()
|
103
108
|
for r in records:
|
104
109
|
session.expunge(r)
|
105
110
|
return records
|
@@ -107,10 +112,15 @@ class DbManager:
|
|
107
112
|
session.rollback()
|
108
113
|
close_session(session)
|
109
114
|
|
110
|
-
def find_first_record(
|
115
|
+
def find_first_record(
|
116
|
+
cls: T, filters: Optional[List], joinedloads: List | str = []
|
117
|
+
) -> T:
|
111
118
|
session = get_session()
|
112
119
|
try:
|
113
|
-
|
120
|
+
query = select(cls)
|
121
|
+
query = _joinedloads(cls, query, joinedloads)
|
122
|
+
query = query.filter(*filters)
|
123
|
+
record = session.scalars(query).unique().first()
|
114
124
|
if record:
|
115
125
|
session.expunge(record)
|
116
126
|
return record
|
File without changes
|