f3-data-models 0.1.7__tar.gz → 0.1.10__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.7
3
+ Version: 0.1.10
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,5 +1,5 @@
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,
@@ -17,6 +17,7 @@ from sqlalchemy.orm import (
17
17
  DeclarativeBase,
18
18
  mapped_column,
19
19
  Mapped,
20
+ relationship,
20
21
  )
21
22
 
22
23
  # Custom Annotations
@@ -282,7 +283,7 @@ class Org(Base):
282
283
 
283
284
  __tablename__ = "orgs"
284
285
 
285
- id: Mapped[intpk]
286
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
286
287
  parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("orgs.id"))
287
288
  org_type_id: Mapped[int] = mapped_column(ForeignKey("org_types.id"))
288
289
  default_location_id: Mapped[Optional[int]]
@@ -300,6 +301,19 @@ class Org(Base):
300
301
  created: Mapped[dt_create]
301
302
  updated: Mapped[dt_update]
302
303
 
304
+ event_types: Mapped[Optional[List["EventType"]]] = relationship(
305
+ "EventType", secondary="event_types_x_org", cascade="expunge"
306
+ )
307
+ event_tags: Mapped[Optional[List["EventTag"]]] = relationship(
308
+ "EventTag", secondary="event_tags_x_org", cascade="expunge"
309
+ )
310
+ achievements: Mapped[Optional[List["Achievement"]]] = relationship(
311
+ "Achievement", secondary="achievements_x_org", cascade="expunge"
312
+ )
313
+ parent_org: Mapped[Optional["Org"]] = relationship(
314
+ "Org", remote_side=[id], cascade="expunge"
315
+ )
316
+
303
317
 
304
318
  class EventType(Base):
305
319
  """
@@ -545,6 +559,15 @@ class Event(Base):
545
559
  created: Mapped[dt_create]
546
560
  updated: Mapped[dt_update]
547
561
 
562
+ org: Mapped[Org] = relationship(innerjoin=True, cascade="expunge")
563
+ location: Mapped[Location] = relationship(innerjoin=True, cascade="expunge")
564
+ event_types: Mapped[List[EventType]] = relationship(
565
+ secondary="events_x_event_types", innerjoin=True, cascade="expunge"
566
+ )
567
+ event_tags: Mapped[Optional[List[EventTag]]] = relationship(
568
+ secondary="event_tags_x_events", cascade="expunge"
569
+ )
570
+
548
571
 
549
572
  class AttendanceType(Base):
550
573
  """
@@ -564,32 +587,6 @@ class AttendanceType(Base):
564
587
  updated: Mapped[dt_update]
565
588
 
566
589
 
567
- class Attendance(Base):
568
- """
569
- Model representing an attendance record.
570
-
571
- Attributes:
572
- id (int): Primary Key of the model.
573
- event_id (int): The ID of the associated event.
574
- user_id (Optional[int]): The ID of the associated user.
575
- is_planned (bool): Whether this is planned attendance (True) vs actual attendance (False).
576
- meta (Optional[Dict[str, Any]]): Additional metadata for the attendance.
577
- created (datetime): The timestamp when the record was created.
578
- updated (datetime): The timestamp when the record was last updated.
579
- """
580
-
581
- __tablename__ = "attendance"
582
- __table_args__ = (UniqueConstraint("event_id", "user_id", "is_planned"),)
583
-
584
- id: Mapped[intpk]
585
- event_id: Mapped[int] = mapped_column(ForeignKey("events.id"))
586
- user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
587
- is_planned: Mapped[bool]
588
- meta: Mapped[Optional[Dict[str, Any]]]
589
- created: Mapped[dt_create]
590
- updated: Mapped[dt_update]
591
-
592
-
593
590
  class Attendance_x_AttenanceType(Base):
594
591
  """
595
592
  Model representing the association between attendance and attendance types.
@@ -692,6 +689,38 @@ class SlackUser(Base):
692
689
  updated: Mapped[dt_update]
693
690
 
694
691
 
692
+ class Attendance(Base):
693
+ """
694
+ Model representing an attendance record.
695
+
696
+ Attributes:
697
+ id (int): Primary Key of the model.
698
+ event_id (int): The ID of the associated event.
699
+ user_id (Optional[int]): The ID of the associated user.
700
+ is_planned (bool): Whether this is planned attendance (True) vs actual attendance (False).
701
+ meta (Optional[Dict[str, Any]]): Additional metadata for the attendance.
702
+ created (datetime): The timestamp when the record was created.
703
+ updated (datetime): The timestamp when the record was last updated.
704
+ """
705
+
706
+ __tablename__ = "attendance"
707
+ __table_args__ = (UniqueConstraint("event_id", "user_id", "is_planned"),)
708
+
709
+ id: Mapped[intpk]
710
+ event_id: Mapped[int] = mapped_column(ForeignKey("events.id"))
711
+ user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
712
+ is_planned: Mapped[bool]
713
+ meta: Mapped[Optional[Dict[str, Any]]]
714
+ created: Mapped[dt_create]
715
+ updated: Mapped[dt_update]
716
+
717
+ event: Mapped[Event] = relationship(innerjoin=True, cascade="expunge")
718
+ user: Mapped[User] = relationship(innerjoin=True, cascade="expunge", viewonly=True)
719
+ slack_user: Mapped[Optional[SlackUser]] = relationship(
720
+ innerjoin=False, cascade="expunge", secondary="users"
721
+ )
722
+
723
+
695
724
  class Achievement(Base):
696
725
  """
697
726
  Model representing an achievement.
@@ -894,3 +923,20 @@ class MagicLinkAuthSession(Base):
894
923
  session_token: Mapped[str]
895
924
  created: Mapped[dt_create]
896
925
  expiration: Mapped[dt_create]
926
+
927
+
928
+ # class Org_x_SlackChannel(Base):
929
+ # """
930
+ # Model representing the association between organizations (specifically AOs) and Slack channels.
931
+
932
+ # Attributes:
933
+ # org_id (int): The ID of the associated organization.
934
+ # slack_channel_id (str): The Slack-internal ID of the associated Slack channel.
935
+ # """
936
+
937
+ # __tablename__ = "orgs_x_slack_channels"
938
+
939
+ # org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"), primary_key=True)
940
+ # slack_channel_id: Mapped[str] = mapped_column(
941
+ # primary_key=True
942
+ # ) # 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])
90
86
 
91
- def get(cls: T, id: int) -> T:
87
+
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]:
99
+ def find_records(
100
+ cls: T, filters: Optional[List], joinedloads: List | str = []
101
+ ) -> List[T]:
100
102
  session = get_session()
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.7"
3
+ version = "0.1.10"
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"