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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: f3-data-models
3
- Version: 0.1.6
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. The github pages documentation will be updated when you push to `main`, but if you would like to preview locally, run:
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. The github pages documentation will be updated when you push to `main`, but if you would like to preview locally, run:
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[intpk]
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
- acronyms (Optional[str]): Acronyms associated with the event type.
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
- acronyms: Mapped[Optional[str]]
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
- Model representing the association between events and event types. The intention is that a single event can be associated with multiple event types.
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
- Attributes:
334
- event_id (int): The ID of the associated event.
335
- event_type_id (int): The ID of the associated event type.
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
- __tablename__ = "events_x_event_types"
378
+ # __tablename__ = "events_x_event_types"
339
379
 
340
- event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), primary_key=True)
341
- event_type_id: Mapped[int] = mapped_column(
342
- ForeignKey("event_types.id"), primary_key=True
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
- 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.
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
- Attributes:
351
- event_type_id (int): The ID of the associated event type.
352
- org_id (int): The ID of the associated organization.
353
- is_default (bool): Whether this is the default event type for the organization. Default is False.
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
- __tablename__ = "event_types_x_org"
396
+ # __tablename__ = "event_types_x_org"
357
397
 
358
- event_type_id: Mapped[int] = mapped_column(
359
- ForeignKey("event_types.id"), primary_key=True
360
- )
361
- org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
362
- is_default: Mapped[bool] = mapped_column(Boolean, default=False)
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
- Model representing the association between event tags and events. The intention is that a single event can be associated with multiple event tags.
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
- Attributes:
393
- event_id (int): The ID of the associated event.
394
- event_tag_id (int): The ID of the associated event tag.
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
- __tablename__ = "event_tags_x_events"
437
+ # __tablename__ = "event_tags_x_events"
398
438
 
399
- event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), primary_key=True)
400
- event_tag_id: Mapped[int] = mapped_column(
401
- ForeignKey("event_tags.id"), primary_key=True
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
- Model representing the association between event tags and organizations. Controls which event tags are available for selection at the region level.
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
- Attributes:
410
- event_tag_id (int): The ID of the associated event tag.
411
- org_id (int): The ID of the associated organization.
412
- color_override (Optional[str]): The calendar color override for the event tag (if the region wants to use something other than the default).
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
- __tablename__ = "event_tags_x_org"
455
+ # __tablename__ = "event_tags_x_org"
416
456
 
417
- event_tag_id: Mapped[int] = mapped_column(
418
- ForeignKey("event_tags.id"), primary_key=True
419
- )
420
- org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
421
- color_override: Mapped[Optional[str]]
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 Org_x_Slack(Base):
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__ = "org_x_slack"
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[str] = mapped_column(
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
- Model representing the association between achievements and organizations.
805
+ # class Achievement_x_Org(Base):
806
+ # """
807
+ # Model representing the association between achievements and organizations.
741
808
 
742
- Attributes:
743
- achievement_id (int): The ID of the associated achievement.
744
- org_id (int): The ID of the associated organization.
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
- __tablename__ = "achievements_x_org"
814
+ # __tablename__ = "achievements_x_org"
748
815
 
749
- achievement_id: Mapped[int] = mapped_column(
750
- ForeignKey("achievements.id"), primary_key=True
751
- )
752
- org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
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
- class DbManager:
80
- def get_record(cls: T, id) -> T:
81
- session = get_session()
82
- try:
83
- x = session.query(cls).filter(cls.get_id() == id).first()
84
- if x:
85
- session.expunge(x)
86
- return x
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
- def get(cls: T, id: int) -> T:
88
+ class DbManager:
89
+ def get(cls: T, id: int, joinedloads: list | str = []) -> T:
92
90
  session = get_session()
93
91
  try:
94
- return session.scalars(select(cls).filter(cls.id == id)).one()
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(cls: T, filters: Optional[List]) -> List[T]:
100
- session = get_session()
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
- records = session.scalars(select(cls).filter(*filters)).all()
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(cls: T, filters: Optional[List]) -> T:
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
- record = session.scalars(select(cls).filter(*filters)).first()
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "f3-data-models"
3
- version = "0.1.6"
3
+ version = "0.1.8"
4
4
  description = "The data schema and models for F3 Nation applications."
5
5
  authors = ["Evan Petzoldt <evan.petzoldt@protonmail.com>"]
6
6
  readme = "README.md"