f3-data-models 0.1.7__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.7 → f3_data_models-0.1.8}/PKG-INFO +17 -2
- {f3_data_models-0.1.7 → f3_data_models-0.1.8}/README.md +16 -1
- {f3_data_models-0.1.7 → f3_data_models-0.1.8}/f3_data_models/models.py +147 -66
- {f3_data_models-0.1.7 → f3_data_models-0.1.8}/f3_data_models/utils.py +30 -20
- {f3_data_models-0.1.7 → f3_data_models-0.1.8}/pyproject.toml +1 -1
- {f3_data_models-0.1.7 → 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
|
"""
|
@@ -326,40 +366,40 @@ class EventType(Base):
|
|
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,40 +425,40 @@ 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
464
|
class Org_x_SlackSpace(Base):
|
@@ -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
|
"""
|
@@ -738,21 +802,21 @@ class Achievement_x_User(Base):
|
|
738
802
|
)
|
739
803
|
|
740
804
|
|
741
|
-
class Achievement_x_Org(Base):
|
742
|
-
|
743
|
-
|
805
|
+
# class Achievement_x_Org(Base):
|
806
|
+
# """
|
807
|
+
# Model representing the association between achievements and organizations.
|
744
808
|
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
809
|
+
# Attributes:
|
810
|
+
# achievement_id (int): The ID of the associated achievement.
|
811
|
+
# org_id (int): The ID of the associated organization.
|
812
|
+
# """
|
749
813
|
|
750
|
-
|
814
|
+
# __tablename__ = "achievements_x_org"
|
751
815
|
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
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)
|
756
820
|
|
757
821
|
|
758
822
|
class Position(Base):
|
@@ -894,3 +958,20 @@ class MagicLinkAuthSession(Base):
|
|
894
958
|
session_token: Mapped[str]
|
895
959
|
created: Mapped[dt_create]
|
896
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
|