f3-data-models 0.1.0__py3-none-any.whl
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.
File without changes
|
f3_data_models/models.py
ADDED
@@ -0,0 +1,889 @@
|
|
1
|
+
from datetime import datetime, date, time
|
2
|
+
from typing import Any, Dict, List, Optional
|
3
|
+
from sqlalchemy import (
|
4
|
+
JSON,
|
5
|
+
TEXT,
|
6
|
+
TIME,
|
7
|
+
VARCHAR,
|
8
|
+
Boolean,
|
9
|
+
DateTime,
|
10
|
+
ForeignKey,
|
11
|
+
Integer,
|
12
|
+
UniqueConstraint,
|
13
|
+
func,
|
14
|
+
)
|
15
|
+
from typing_extensions import Annotated
|
16
|
+
from sqlalchemy.orm import relationship, DeclarativeBase, mapped_column, Mapped
|
17
|
+
|
18
|
+
# Custom Annotations
|
19
|
+
time_notz = Annotated[time, TIME]
|
20
|
+
text = Annotated[str, TEXT]
|
21
|
+
|
22
|
+
|
23
|
+
class Base(DeclarativeBase):
|
24
|
+
"""
|
25
|
+
Base class for all models, providing common fields and methods.
|
26
|
+
|
27
|
+
Attributes:
|
28
|
+
id (int): Primary key of the model.
|
29
|
+
created (datetime): Timestamp when the model was created.
|
30
|
+
updated (datetime): Timestamp when the model was last updated.
|
31
|
+
"""
|
32
|
+
|
33
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
34
|
+
# created: Mapped[datetime] = dt_create
|
35
|
+
# updated: Mapped[datetime] = dt_update
|
36
|
+
created: Mapped[datetime] = mapped_column(
|
37
|
+
DateTime, server_default=func.timezone("utc", func.now())
|
38
|
+
)
|
39
|
+
updated: Mapped[datetime] = mapped_column(
|
40
|
+
DateTime,
|
41
|
+
server_default=func.timezone("utc", func.now()),
|
42
|
+
onupdate=func.timezone("utc", func.now()),
|
43
|
+
)
|
44
|
+
|
45
|
+
type_annotation_map = {
|
46
|
+
Dict[str, Any]: JSON,
|
47
|
+
}
|
48
|
+
|
49
|
+
def get_id(self):
|
50
|
+
"""
|
51
|
+
Get the primary key of the model.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
int: The primary key of the model.
|
55
|
+
"""
|
56
|
+
return self.id
|
57
|
+
|
58
|
+
def get(self, attr):
|
59
|
+
"""
|
60
|
+
Get the value of a specified attribute.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
attr (str): The name of the attribute.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Any: The value of the attribute if it exists, otherwise None.
|
67
|
+
"""
|
68
|
+
if attr in [c.key for c in self.__table__.columns]:
|
69
|
+
return getattr(self, attr)
|
70
|
+
return None
|
71
|
+
|
72
|
+
def to_json(self):
|
73
|
+
"""
|
74
|
+
Convert the model instance to a JSON-serializable dictionary.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
dict: A dictionary representation of the model instance.
|
78
|
+
"""
|
79
|
+
return {
|
80
|
+
c.key: self.get(c.key)
|
81
|
+
for c in self.__table__.columns
|
82
|
+
if c.key not in ["created", "updated"]
|
83
|
+
}
|
84
|
+
|
85
|
+
def __repr__(self):
|
86
|
+
"""
|
87
|
+
Get a string representation of the model instance.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
str: A string representation of the model instance.
|
91
|
+
"""
|
92
|
+
return str(self.to_json())
|
93
|
+
|
94
|
+
def _update(self, fields):
|
95
|
+
"""
|
96
|
+
Update the model instance with the provided fields.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
fields (dict): A dictionary of fields to update.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Base: The updated model instance.
|
103
|
+
"""
|
104
|
+
for k, v in fields.items():
|
105
|
+
attr_name = str(k).split(".")[-1]
|
106
|
+
setattr(self, attr_name, v)
|
107
|
+
return self
|
108
|
+
|
109
|
+
|
110
|
+
class SlackSpace(Base):
|
111
|
+
"""
|
112
|
+
Model representing a Slack workspace.
|
113
|
+
|
114
|
+
Attributes:
|
115
|
+
team_id (str): The Slack-internal unique identifier for the Slack team.
|
116
|
+
workspace_name (Optional[str]): The name of the Slack workspace.
|
117
|
+
bot_token (Optional[str]): The bot token for the Slack workspace.
|
118
|
+
settings (Optional[Dict[str, Any]]): Slack Bot settings for the Slack workspace.
|
119
|
+
|
120
|
+
org_x_slack (Org_x_Slack): The organization associated with this Slack workspace.
|
121
|
+
"""
|
122
|
+
|
123
|
+
__tablename__ = "slack_spaces"
|
124
|
+
|
125
|
+
team_id: Mapped[str] = mapped_column(VARCHAR, unique=True)
|
126
|
+
workspace_name: Mapped[Optional[str]]
|
127
|
+
bot_token: Mapped[Optional[str]]
|
128
|
+
settings: Mapped[Optional[Dict[str, Any]]]
|
129
|
+
|
130
|
+
org_x_slack: Mapped["Org_x_Slack"] = relationship(back_populates="slack_space")
|
131
|
+
|
132
|
+
|
133
|
+
class OrgType(Base):
|
134
|
+
"""
|
135
|
+
Model representing an organization type / level. 1=AO, 2=Region, 3=Area, 4=Sector
|
136
|
+
|
137
|
+
Attributes:
|
138
|
+
name (str): The name of the organization type.
|
139
|
+
description (Optional[text]): A description of the organization type.
|
140
|
+
"""
|
141
|
+
|
142
|
+
__tablename__ = "org_types"
|
143
|
+
|
144
|
+
name: Mapped[str]
|
145
|
+
description: Mapped[Optional[text]]
|
146
|
+
|
147
|
+
|
148
|
+
class EventCategory(Base):
|
149
|
+
"""
|
150
|
+
Model representing an event category. These are immutable cateogies that we will define at the Nation level.
|
151
|
+
|
152
|
+
Attributes:
|
153
|
+
name (str): The name of the event category.
|
154
|
+
description (Optional[text]): A description of the event category.
|
155
|
+
event_types (List[EventType]): A list of event types associated with this category.
|
156
|
+
"""
|
157
|
+
|
158
|
+
__tablename__ = "event_categories"
|
159
|
+
|
160
|
+
name: Mapped[str]
|
161
|
+
description: Mapped[Optional[text]]
|
162
|
+
|
163
|
+
event_types: Mapped[List["EventType"]] = relationship(
|
164
|
+
back_populates="event_category"
|
165
|
+
)
|
166
|
+
|
167
|
+
|
168
|
+
class Role(Base):
|
169
|
+
"""
|
170
|
+
Model representing a role. A role is a set of permissions that can be assigned to users.
|
171
|
+
|
172
|
+
Attributes:
|
173
|
+
name (str): The name of the role.
|
174
|
+
description (Optional[text]): A description of the role.
|
175
|
+
"""
|
176
|
+
|
177
|
+
__tablename__ = "roles"
|
178
|
+
|
179
|
+
name: Mapped[str]
|
180
|
+
description: Mapped[Optional[text]]
|
181
|
+
|
182
|
+
|
183
|
+
class Permission(Base):
|
184
|
+
"""
|
185
|
+
Model representing a permission.
|
186
|
+
|
187
|
+
Attributes:
|
188
|
+
name (str): The name of the permission.
|
189
|
+
description (Optional[text]): A description of the permission.
|
190
|
+
"""
|
191
|
+
|
192
|
+
__tablename__ = "permissions"
|
193
|
+
|
194
|
+
name: Mapped[str]
|
195
|
+
description: Mapped[Optional[text]]
|
196
|
+
|
197
|
+
|
198
|
+
class Role_x_Permission(Base):
|
199
|
+
"""
|
200
|
+
Model representing the assignment of permissions to roles.
|
201
|
+
|
202
|
+
Attributes:
|
203
|
+
role_id (int): The ID of the associated role.
|
204
|
+
permission_id (int): The ID of the associated permission.
|
205
|
+
role (Role): The role associated with this relationship.
|
206
|
+
permission (Permission): The permission associated with this relationship.
|
207
|
+
"""
|
208
|
+
|
209
|
+
__tablename__ = "roles_x_permissions"
|
210
|
+
__table_args__ = (
|
211
|
+
UniqueConstraint("role_id", "permission_id", name="_role_permission_uc"),
|
212
|
+
)
|
213
|
+
|
214
|
+
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
|
215
|
+
permission_id: Mapped[int] = mapped_column(ForeignKey("permissions.id"))
|
216
|
+
|
217
|
+
role: Mapped["Role"] = relationship(back_populates="role_x_permission")
|
218
|
+
permissions: Mapped[List["Permission"]] = relationship(
|
219
|
+
back_populates="role_x_permission"
|
220
|
+
)
|
221
|
+
|
222
|
+
|
223
|
+
class Role_x_User_x_Org(Base):
|
224
|
+
"""
|
225
|
+
Model representing the assignment of roles, users, and organizations.
|
226
|
+
|
227
|
+
Attributes:
|
228
|
+
role_id (int): The ID of the associated role.
|
229
|
+
user_id (int): The ID of the associated user.
|
230
|
+
org_id (int): The ID of the associated organization.
|
231
|
+
role (Role): The role associated with this relationship.
|
232
|
+
user (User): The user associated with this relationship.
|
233
|
+
org (Org): The organization associated with this relationship.
|
234
|
+
"""
|
235
|
+
|
236
|
+
__tablename__ = "roles_x_users_x_org"
|
237
|
+
__table_args__ = (
|
238
|
+
UniqueConstraint("role_id", "user_id", "org_id", name="_role_user_org_uc"),
|
239
|
+
)
|
240
|
+
|
241
|
+
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
|
242
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
243
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
244
|
+
|
245
|
+
role: Mapped["Role"] = relationship(back_populates="role_x_user_x_org")
|
246
|
+
user: Mapped["User"] = relationship(back_populates="role_x_user_x_org")
|
247
|
+
org: Mapped["Org"] = relationship(back_populates="role_x_user_x_org")
|
248
|
+
|
249
|
+
|
250
|
+
class Org(Base):
|
251
|
+
"""
|
252
|
+
Model representing an organization. The same model is used for all levels of organization (AOs, Regions, etc.).
|
253
|
+
|
254
|
+
Attributes:
|
255
|
+
parent_id (Optional[int]): The ID of the parent organization.
|
256
|
+
org_type_id (int): The ID of the organization type.
|
257
|
+
default_location_id (Optional[int]): The ID of the default location.
|
258
|
+
name (str): The name of the organization.
|
259
|
+
description (Optional[text]): A description of the organization.
|
260
|
+
is_active (bool): Whether the organization is active.
|
261
|
+
logo_url (Optional[str]): The URL of the organization's logo.
|
262
|
+
website (Optional[str]): The organization's website.
|
263
|
+
email (Optional[str]): The organization's email.
|
264
|
+
twitter (Optional[str]): The organization's Twitter handle.
|
265
|
+
facebook (Optional[str]): The organization's Facebook page.
|
266
|
+
instagram (Optional[str]): The organization's Instagram handle.
|
267
|
+
last_annual_review (Optional[date]): The date of the last annual review.
|
268
|
+
meta (Optional[Dict[str, Any]]): Additional metadata for the organization.
|
269
|
+
parent_org (Optional[Org]): The parent organization.
|
270
|
+
child_orgs (List[Org]): The child organizations.
|
271
|
+
locations (List[Location]): The locations associated with the organization.
|
272
|
+
"""
|
273
|
+
|
274
|
+
__tablename__ = "orgs"
|
275
|
+
|
276
|
+
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("orgs.id"))
|
277
|
+
org_type_id: Mapped[int] = mapped_column(ForeignKey("org_types.id"))
|
278
|
+
default_location_id: Mapped[Optional[int]]
|
279
|
+
name: Mapped[str]
|
280
|
+
description: Mapped[Optional[text]]
|
281
|
+
is_active: Mapped[bool]
|
282
|
+
logo_url: Mapped[Optional[str]]
|
283
|
+
website: Mapped[Optional[str]]
|
284
|
+
email: Mapped[Optional[str]]
|
285
|
+
twitter: Mapped[Optional[str]]
|
286
|
+
facebook: Mapped[Optional[str]]
|
287
|
+
instagram: Mapped[Optional[str]]
|
288
|
+
last_annual_review: Mapped[Optional[date]]
|
289
|
+
meta: Mapped[Optional[Dict[str, Any]]]
|
290
|
+
|
291
|
+
parent_org: Mapped[Optional["Org"]] = relationship(
|
292
|
+
"Org", remote_side="Org.id", back_populates="child_orgs"
|
293
|
+
)
|
294
|
+
child_orgs: Mapped[List["Org"]] = relationship(
|
295
|
+
"Org", back_populates="parent_org", join_depth=3
|
296
|
+
)
|
297
|
+
locations: Mapped[List["Location"]] = relationship(back_populates="org")
|
298
|
+
|
299
|
+
|
300
|
+
class EventType(Base):
|
301
|
+
"""
|
302
|
+
Model representing an event type. Event types can be shared by regions or not, and should roll up into event categories.
|
303
|
+
|
304
|
+
Attributes:
|
305
|
+
name (str): The name of the event type.
|
306
|
+
description (Optional[text]): A description of the event type.
|
307
|
+
acronyms (Optional[str]): Acronyms associated with the event type.
|
308
|
+
category_id (int): The ID of the associated event category.
|
309
|
+
event_category (EventCategory): The event category associated with this event type.
|
310
|
+
"""
|
311
|
+
|
312
|
+
__tablename__ = "event_types"
|
313
|
+
|
314
|
+
name: Mapped[str]
|
315
|
+
description: Mapped[Optional[text]]
|
316
|
+
acronyms: Mapped[Optional[str]]
|
317
|
+
category_id: Mapped[int] = mapped_column(ForeignKey("event_categories.id"))
|
318
|
+
|
319
|
+
event_category: Mapped["EventCategory"] = relationship(back_populates="event_types")
|
320
|
+
|
321
|
+
|
322
|
+
class EventType_x_Event(Base):
|
323
|
+
"""
|
324
|
+
Model representing the association between events and event types. The intention is that a single event can be associated with multiple event types.
|
325
|
+
|
326
|
+
Attributes:
|
327
|
+
event_id (int): The ID of the associated event.
|
328
|
+
event_type_id (int): The ID of the associated event type.
|
329
|
+
event (Event): The event associated with this relationship.
|
330
|
+
event_type (EventType): The event type associated with this relationship.
|
331
|
+
"""
|
332
|
+
|
333
|
+
__tablename__ = "events_x_event_types"
|
334
|
+
__table_args__ = (
|
335
|
+
UniqueConstraint("event_id", "event_type_id", name="_event_event_type_uc"),
|
336
|
+
)
|
337
|
+
|
338
|
+
event_id: Mapped[int] = mapped_column(ForeignKey("events.id"))
|
339
|
+
event_type_id: Mapped[int] = mapped_column(ForeignKey("event_types.id"))
|
340
|
+
|
341
|
+
event: Mapped["Event"] = relationship(back_populates="events_x_event_types")
|
342
|
+
event_type: Mapped["EventType"] = relationship(
|
343
|
+
back_populates="events_x_event_types"
|
344
|
+
)
|
345
|
+
|
346
|
+
|
347
|
+
class EventType_x_Org(Base):
|
348
|
+
"""
|
349
|
+
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.
|
350
|
+
|
351
|
+
Attributes:
|
352
|
+
event_type_id (int): The ID of the associated event type.
|
353
|
+
org_id (int): The ID of the associated organization.
|
354
|
+
is_default (bool): Whether this is the default event type for the organization.
|
355
|
+
event_type (EventType): The event type associated with this relationship.
|
356
|
+
org (Org): The organization associated with this relationship.
|
357
|
+
"""
|
358
|
+
|
359
|
+
__tablename__ = "event_types_x_org"
|
360
|
+
__table_args__ = (
|
361
|
+
UniqueConstraint("event_type_id", "org_id", name="_event_type_org_uc"),
|
362
|
+
)
|
363
|
+
|
364
|
+
event_type_id: Mapped[int] = mapped_column(ForeignKey("event_types.id"))
|
365
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
366
|
+
is_default: Mapped[bool]
|
367
|
+
|
368
|
+
event_type: Mapped["EventType"] = relationship(back_populates="event_type_x_org")
|
369
|
+
org: Mapped["Org"] = relationship(back_populates="event_type_x_org")
|
370
|
+
|
371
|
+
|
372
|
+
class EventTag(Base):
|
373
|
+
"""
|
374
|
+
Model representing an event tag. These are used to mark special events, such as anniversaries or special workouts.
|
375
|
+
|
376
|
+
Attributes:
|
377
|
+
name (str): The name of the event tag.
|
378
|
+
description (Optional[text]): A description of the event tag.
|
379
|
+
color (Optional[str]): The color used for the calendar.
|
380
|
+
"""
|
381
|
+
|
382
|
+
__tablename__ = "event_tags"
|
383
|
+
|
384
|
+
name: Mapped[str]
|
385
|
+
description: Mapped[Optional[text]]
|
386
|
+
color: Mapped[Optional[str]]
|
387
|
+
|
388
|
+
|
389
|
+
class EventTag_x_Event(Base):
|
390
|
+
"""
|
391
|
+
Model representing the association between event tags and events. The intention is that a single event can be associated with multiple event tags.
|
392
|
+
|
393
|
+
Attributes:
|
394
|
+
event_id (int): The ID of the associated event.
|
395
|
+
event_tag_id (int): The ID of the associated event tag.
|
396
|
+
event (Event): The event associated with this relationship.
|
397
|
+
event_tag (EventTag): The event tag associated with this relationship.
|
398
|
+
"""
|
399
|
+
|
400
|
+
__tablename__ = "event_tags_x_events"
|
401
|
+
__table_args__ = (
|
402
|
+
UniqueConstraint("event_id", "event_tag_id", name="_event_event_tag_uc"),
|
403
|
+
)
|
404
|
+
|
405
|
+
event_id: Mapped[int] = mapped_column(ForeignKey("events.id"))
|
406
|
+
event_tag_id: Mapped[int] = mapped_column(ForeignKey("event_tags.id"))
|
407
|
+
|
408
|
+
event: Mapped["Event"] = relationship(back_populates="event_tag_x_event")
|
409
|
+
event_tags: Mapped["EventTag"] = relationship(back_populates="event_tag_x_event")
|
410
|
+
|
411
|
+
|
412
|
+
class EventTag_x_Org(Base):
|
413
|
+
"""
|
414
|
+
Model representing the association between event tags and organizations. Controls which event tags are available for selection at the region level.
|
415
|
+
|
416
|
+
Attributes:
|
417
|
+
event_tag_id (int): The ID of the associated event tag.
|
418
|
+
org_id (int): The ID of the associated organization.
|
419
|
+
color_override (Optional[str]): The color override for the event tag (if the region wants to use something other than the default).
|
420
|
+
event_tag (EventTag): The event tag associated with this relationship.
|
421
|
+
org (Org): The organization associated with this relationship.
|
422
|
+
"""
|
423
|
+
|
424
|
+
__tablename__ = "event_tags_x_org"
|
425
|
+
__table_args__ = (
|
426
|
+
UniqueConstraint("event_tag_id", "org_id", name="_event_tag_org_uc"),
|
427
|
+
)
|
428
|
+
|
429
|
+
event_tag_id: Mapped[int] = mapped_column(ForeignKey("event_tags.id"))
|
430
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
431
|
+
color_override: Mapped[Optional[str]]
|
432
|
+
|
433
|
+
event_tag: Mapped["EventTag"] = relationship(back_populates="event_tag_x_org")
|
434
|
+
org: Mapped["Org"] = relationship(back_populates="event_tag_x_org")
|
435
|
+
|
436
|
+
|
437
|
+
class Org_x_Slack(Base):
|
438
|
+
"""
|
439
|
+
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.
|
440
|
+
|
441
|
+
Attributes:
|
442
|
+
org_id (int): The ID of the associated organization.
|
443
|
+
slack_space_id (str): The ID of the associated Slack workspace.
|
444
|
+
slack_space (SlackSpace): The Slack workspace associated with this relationship.
|
445
|
+
org (Org): The organization associated with this relationship.
|
446
|
+
"""
|
447
|
+
|
448
|
+
__tablename__ = "org_x_slack"
|
449
|
+
__table_args__ = (
|
450
|
+
UniqueConstraint("org_id", "slack_space_id", name="_org_slack_uc"),
|
451
|
+
)
|
452
|
+
|
453
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
454
|
+
slack_space_id: Mapped[str] = mapped_column(ForeignKey("slack_spaces.id"))
|
455
|
+
|
456
|
+
slack_space: Mapped["SlackSpace"] = relationship(back_populates="org_x_slack")
|
457
|
+
org: Mapped["Org"] = relationship(back_populates="org_x_slack")
|
458
|
+
|
459
|
+
|
460
|
+
class Location(Base):
|
461
|
+
"""
|
462
|
+
Model representing a location. Locations are expected to belong to a single organization (region).
|
463
|
+
|
464
|
+
Attributes:
|
465
|
+
org_id (int): The ID of the associated organization.
|
466
|
+
name (str): The name of the location.
|
467
|
+
description (Optional[text]): A description of the location.
|
468
|
+
is_active (bool): Whether the location is active.
|
469
|
+
lat (Optional[float]): The latitude of the location.
|
470
|
+
lon (Optional[float]): The longitude of the location.
|
471
|
+
address_street (Optional[str]): The street address of the location.
|
472
|
+
address_city (Optional[str]): The city of the location.
|
473
|
+
address_state (Optional[str]): The state of the location.
|
474
|
+
address_zip (Optional[str]): The ZIP code of the location.
|
475
|
+
address_country (Optional[str]): The country of the location.
|
476
|
+
meta (Optional[Dict[str, Any]]): Additional metadata for the location.
|
477
|
+
org (Org): The organization associated with this location.
|
478
|
+
"""
|
479
|
+
|
480
|
+
__tablename__ = "locations"
|
481
|
+
|
482
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
483
|
+
name: Mapped[str]
|
484
|
+
description: Mapped[Optional[text]]
|
485
|
+
is_active: Mapped[bool]
|
486
|
+
lat: Mapped[Optional[float]]
|
487
|
+
lon: Mapped[Optional[float]]
|
488
|
+
address_street: Mapped[Optional[str]]
|
489
|
+
address_city: Mapped[Optional[str]]
|
490
|
+
address_state: Mapped[Optional[str]]
|
491
|
+
address_zip: Mapped[Optional[str]]
|
492
|
+
address_country: Mapped[Optional[str]]
|
493
|
+
meta: Mapped[Optional[Dict[str, Any]]]
|
494
|
+
|
495
|
+
org: Mapped["Org"] = relationship(back_populates="locations")
|
496
|
+
|
497
|
+
|
498
|
+
class Event(Base):
|
499
|
+
"""
|
500
|
+
Model representing an event or series; the same model is used for both with a self-referential relationship for series.
|
501
|
+
|
502
|
+
Attributes:
|
503
|
+
org_id (int): The ID of the associated organization.
|
504
|
+
location_id (Optional[int]): The ID of the associated location.
|
505
|
+
series_id (Optional[int]): The ID of the associated event series.
|
506
|
+
is_series (bool): Whether this record is a series or single occurrence. Default is False.
|
507
|
+
is_active (bool): Whether the event is active. Default is True.
|
508
|
+
highlight (bool): Whether the event is highlighted. Default is False.
|
509
|
+
start_date (date): The start date of the event.
|
510
|
+
end_date (Optional[date]): The end date of the event.
|
511
|
+
start_time (Optional[time_notz]): The start time of the event.
|
512
|
+
end_time (Optional[time_notz]): The end time of the event.
|
513
|
+
day_of_week (Optional[int]): The day of the week of the event. (0=Monday, 6=Sunday)
|
514
|
+
name (str): The name of the event.
|
515
|
+
description (Optional[text]): A description of the event.
|
516
|
+
recurrence_pattern (Optional[str]): The recurrence pattern of the event. Current options are 'weekly' or 'monthly'.
|
517
|
+
recurrence_interval (Optional[int]): The recurrence interval of the event (e.g. every 2 weeks).
|
518
|
+
index_within_interval (Optional[int]): The index within the recurrence interval. (e.g. 2nd Tuesday of the month).
|
519
|
+
pax_count (Optional[int]): The number of participants.
|
520
|
+
fng_count (Optional[int]): The number of first-time participants.
|
521
|
+
preblast (Optional[text]): The pre-event announcement.
|
522
|
+
backblast (Optional[text]): The post-event report.
|
523
|
+
preblast_rich (Optional[Dict[str, Any]]): The rich text pre-event announcement (e.g. Slack message).
|
524
|
+
backblast_rich (Optional[Dict[str, Any]]): The rich text post-event report (e.g. Slack message).
|
525
|
+
preblast_ts (Optional[float]): The Slack post timestamp of the pre-event announcement.
|
526
|
+
backblast_ts (Optional[float]): The Slack post timestamp of the post-event report.
|
527
|
+
meta (Optional[Dict[str, Any]]): Additional metadata for the event.
|
528
|
+
org (Org): The organization associated with this event.
|
529
|
+
location (Location): The location associated with this event.
|
530
|
+
event_type (EventType): The event type associated with this event.
|
531
|
+
event_tag (EventTag): Any event tags associated with this event.
|
532
|
+
series (Event): The event series associated with this event.
|
533
|
+
attendance (List[Attendance]): The attendance records for this event.
|
534
|
+
event_tags_x_event (List[EventTag_x_Event]): The event tags associated with this event.
|
535
|
+
event_types_x_event (List[EventType_x_Event]): The event types associated with this event.
|
536
|
+
"""
|
537
|
+
|
538
|
+
__tablename__ = "events"
|
539
|
+
|
540
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
541
|
+
location_id: Mapped[Optional[int]] = mapped_column(ForeignKey("locations.id"))
|
542
|
+
series_id: Mapped[Optional[int]] = mapped_column(ForeignKey("events.id"))
|
543
|
+
is_series: Mapped[bool] = mapped_column(Boolean, default=False)
|
544
|
+
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
545
|
+
highlight: Mapped[bool] = mapped_column(Boolean, default=False)
|
546
|
+
start_date: Mapped[date]
|
547
|
+
end_date: Mapped[Optional[date]]
|
548
|
+
start_time: Mapped[Optional[time_notz]]
|
549
|
+
end_time: Mapped[Optional[time_notz]]
|
550
|
+
day_of_week: Mapped[Optional[int]]
|
551
|
+
name: Mapped[str]
|
552
|
+
description: Mapped[Optional[text]]
|
553
|
+
recurrence_pattern: Mapped[Optional[str]]
|
554
|
+
recurrence_interval: Mapped[Optional[int]]
|
555
|
+
index_within_interval: Mapped[Optional[int]]
|
556
|
+
pax_count: Mapped[Optional[int]]
|
557
|
+
fng_count: Mapped[Optional[int]]
|
558
|
+
preblast: Mapped[Optional[text]]
|
559
|
+
backblast: Mapped[Optional[text]]
|
560
|
+
preblast_rich: Mapped[Optional[Dict[str, Any]]]
|
561
|
+
backblast_rich: Mapped[Optional[Dict[str, Any]]]
|
562
|
+
preblast_ts: Mapped[Optional[float]]
|
563
|
+
backblast_ts: Mapped[Optional[float]]
|
564
|
+
meta: Mapped[Optional[Dict[str, Any]]]
|
565
|
+
|
566
|
+
org: Mapped["Org"] = relationship(back_populates="events")
|
567
|
+
location: Mapped["Location"] = relationship(back_populates="events")
|
568
|
+
series: Mapped["Event"] = relationship(
|
569
|
+
back_populates="events", remote_side="Event.id"
|
570
|
+
)
|
571
|
+
occurences: Mapped[List["Event"]] = relationship(back_populates="series")
|
572
|
+
attendance: Mapped[List["Attendance"]] = relationship(back_populates="events")
|
573
|
+
event_tags_x_event: Mapped[List["EventTag_x_Event"]] = relationship(
|
574
|
+
back_populates="events"
|
575
|
+
)
|
576
|
+
event_types_x_event: Mapped[List["EventType_x_Event"]] = relationship(
|
577
|
+
back_populates="events"
|
578
|
+
)
|
579
|
+
|
580
|
+
|
581
|
+
class AttendanceType(Base):
|
582
|
+
"""
|
583
|
+
Model representing an attendance type. Basic types are 1='PAX', 2='Q', 3='Co-Q'
|
584
|
+
|
585
|
+
Attributes:
|
586
|
+
type (str): The type of attendance.
|
587
|
+
description (Optional[str]): A description of the attendance type.
|
588
|
+
"""
|
589
|
+
|
590
|
+
__tablename__ = "attendance_types"
|
591
|
+
|
592
|
+
type: Mapped[str]
|
593
|
+
description: Mapped[Optional[str]]
|
594
|
+
|
595
|
+
|
596
|
+
class Attendance(Base):
|
597
|
+
"""
|
598
|
+
Model representing an attendance record.
|
599
|
+
|
600
|
+
Attributes:
|
601
|
+
event_id (int): The ID of the associated event.
|
602
|
+
user_id (Optional[int]): The ID of the associated user.
|
603
|
+
attendance_type_id (int): The ID of the associated attendance type.
|
604
|
+
is_planned (bool): Whether this is planned attendance (True) vs actual attendance (False).
|
605
|
+
meta (Optional[Dict[str, Any]]): Additional metadata for the attendance.
|
606
|
+
event (Event): The event associated with this attendance.
|
607
|
+
user (User): The user associated with this attendance.
|
608
|
+
attendance_type (AttendanceType): The attendance type associated with this attendance.
|
609
|
+
"""
|
610
|
+
|
611
|
+
__tablename__ = "attendance"
|
612
|
+
__table_args__ = (
|
613
|
+
UniqueConstraint(
|
614
|
+
"event_id", "user_id", "is_planned", name="_event_user_planned_uc"
|
615
|
+
),
|
616
|
+
)
|
617
|
+
|
618
|
+
event_id: Mapped[int] = mapped_column(ForeignKey("events.id"))
|
619
|
+
user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"))
|
620
|
+
attendance_type_id: Mapped[int] = mapped_column(ForeignKey("attendance_types.id"))
|
621
|
+
is_planned: Mapped[bool]
|
622
|
+
meta: Mapped[Optional[Dict[str, Any]]]
|
623
|
+
|
624
|
+
event: Mapped["Event"] = relationship(back_populates="attendance")
|
625
|
+
user: Mapped["User"] = relationship(back_populates="attendance")
|
626
|
+
attendance_type: Mapped["AttendanceType"] = relationship(
|
627
|
+
back_populates="attendance"
|
628
|
+
)
|
629
|
+
|
630
|
+
|
631
|
+
class User(Base):
|
632
|
+
"""
|
633
|
+
Model representing a user.
|
634
|
+
|
635
|
+
Attributes:
|
636
|
+
f3_name (Optional[str]): The F3 name of the user.
|
637
|
+
first_name (Optional[str]): The first name of the user.
|
638
|
+
last_name (Optional[str]): The last name of the user.
|
639
|
+
email (str): The email of the user.
|
640
|
+
phone (Optional[str]): The phone number of the user.
|
641
|
+
home_region_id (Optional[int]): The ID of the home region.
|
642
|
+
avatar_url (Optional[str]): The URL of the user's avatar.
|
643
|
+
meta (Optional[Dict[str, Any]]): Additional metadata for the user.
|
644
|
+
home_region (Org): The home region associated with this user.
|
645
|
+
attendance (List[Attendance]): The attendance records for this user.
|
646
|
+
slack_users (List[SlackUser]): The Slack users associated with this user.
|
647
|
+
achievements_x_user (List[Achievement_x_User]): The achievements associated with this user.
|
648
|
+
positions_x_orgs_x_users (List[Position_x_Org_x_User]): The positions associated with this user.
|
649
|
+
roles_x_users_x_org (List[Role_x_User_x_Org]): The roles associated with this user.
|
650
|
+
"""
|
651
|
+
|
652
|
+
__tablename__ = "users"
|
653
|
+
|
654
|
+
f3_name: Mapped[Optional[str]]
|
655
|
+
first_name: Mapped[Optional[str]]
|
656
|
+
last_name: Mapped[Optional[str]]
|
657
|
+
email: Mapped[str] = mapped_column(VARCHAR, unique=True)
|
658
|
+
phone: Mapped[Optional[str]]
|
659
|
+
home_region_id: Mapped[Optional[int]] = mapped_column(ForeignKey("orgs.id"))
|
660
|
+
avatar_url: Mapped[Optional[str]]
|
661
|
+
meta: Mapped[Optional[Dict[str, Any]]]
|
662
|
+
|
663
|
+
home_region: Mapped["Org"] = relationship(back_populates="users")
|
664
|
+
attendance: Mapped[List["Attendance"]] = relationship(back_populates="users")
|
665
|
+
slack_users: Mapped[List["SlackUser"]] = relationship(back_populates="users")
|
666
|
+
achievements_x_user: Mapped[List["Achievement_x_User"]] = relationship(
|
667
|
+
back_populates="user"
|
668
|
+
)
|
669
|
+
positions_x_orgs_x_users: Mapped[List["Position_x_Org_x_User"]] = relationship(
|
670
|
+
back_populates="user"
|
671
|
+
)
|
672
|
+
roles_x_users_x_org: Mapped[List["Role_x_User_x_Org"]] = relationship(
|
673
|
+
back_populates="user"
|
674
|
+
)
|
675
|
+
|
676
|
+
|
677
|
+
class SlackUser(Base):
|
678
|
+
"""
|
679
|
+
Model representing a Slack user.
|
680
|
+
|
681
|
+
Attributes:
|
682
|
+
slack_id (str): The Slack ID of the user.
|
683
|
+
user_name (str): The username of the Slack user.
|
684
|
+
email (str): The email of the Slack user.
|
685
|
+
is_admin (bool): Whether the user is an admin.
|
686
|
+
is_owner (bool): Whether the user is the owner.
|
687
|
+
is_bot (bool): Whether the user is a bot.
|
688
|
+
user_id (Optional[int]): The ID of the associated user.
|
689
|
+
avatar_url (Optional[str]): The URL of the user's avatar.
|
690
|
+
slack_team_id (str): The ID of the associated Slack team.
|
691
|
+
strava_access_token (Optional[str]): The Strava access token of the user.
|
692
|
+
strava_refresh_token (Optional[str]): The Strava refresh token of the user.
|
693
|
+
strava_expires_at (Optional[datetime]): The expiration time of the Strava token.
|
694
|
+
strava_athlete_id (Optional[int]): The Strava athlete ID of the user.
|
695
|
+
meta (Optional[Dict[str, Any]]): Additional metadata for the Slack user.
|
696
|
+
slack_updated (Optional[datetime]): The last update time of the Slack user.
|
697
|
+
slack_space (SlackSpace): The Slack workspace associated with this user.
|
698
|
+
user (User): The user associated with this Slack user.
|
699
|
+
"""
|
700
|
+
|
701
|
+
__tablename__ = "slack_users"
|
702
|
+
|
703
|
+
slack_id: Mapped[str]
|
704
|
+
user_name: Mapped[str]
|
705
|
+
email: Mapped[str]
|
706
|
+
is_admin: Mapped[bool]
|
707
|
+
is_owner: Mapped[bool]
|
708
|
+
is_bot: Mapped[bool]
|
709
|
+
user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"))
|
710
|
+
avatar_url: Mapped[Optional[str]]
|
711
|
+
slack_team_id: Mapped[str] = mapped_column(ForeignKey("slack_spaces.team_id"))
|
712
|
+
strava_access_token: Mapped[Optional[str]]
|
713
|
+
strava_refresh_token: Mapped[Optional[str]]
|
714
|
+
strava_expires_at: Mapped[Optional[datetime]]
|
715
|
+
strava_athlete_id: Mapped[Optional[int]]
|
716
|
+
meta: Mapped[Optional[Dict[str, Any]]]
|
717
|
+
slack_updated: Mapped[Optional[datetime]]
|
718
|
+
|
719
|
+
slack_space: Mapped["SlackSpace"] = relationship(back_populates="slack_users")
|
720
|
+
user: Mapped["User"] = relationship(back_populates="slack_users")
|
721
|
+
|
722
|
+
|
723
|
+
class Achievement(Base):
|
724
|
+
"""
|
725
|
+
Model representing an achievement.
|
726
|
+
|
727
|
+
Attributes:
|
728
|
+
name (str): The name of the achievement.
|
729
|
+
description (Optional[str]): A description of the achievement.
|
730
|
+
verb (str): The verb associated with the achievement.
|
731
|
+
image_url (Optional[str]): The URL of the achievement's image.
|
732
|
+
"""
|
733
|
+
|
734
|
+
__tablename__ = "achievements"
|
735
|
+
|
736
|
+
name: Mapped[str]
|
737
|
+
description: Mapped[Optional[str]]
|
738
|
+
verb: Mapped[str]
|
739
|
+
image_url: Mapped[Optional[str]]
|
740
|
+
|
741
|
+
|
742
|
+
class Achievement_x_User(Base):
|
743
|
+
"""
|
744
|
+
Model representing the association between achievements and users.
|
745
|
+
|
746
|
+
Attributes:
|
747
|
+
achievement_id (int): The ID of the associated achievement.
|
748
|
+
user_id (int): The ID of the associated user.
|
749
|
+
date_awarded (date): The date the achievement was awarded.
|
750
|
+
achievement (Achievement): The achievement associated with this relationship.
|
751
|
+
user (User): The user associated with this relationship.
|
752
|
+
"""
|
753
|
+
|
754
|
+
__tablename__ = "achievements_x_users"
|
755
|
+
__table_args__ = (
|
756
|
+
UniqueConstraint("achievement_id", "user_id", name="_achievement_user_uc"),
|
757
|
+
)
|
758
|
+
|
759
|
+
achievement_id: Mapped[int] = mapped_column(ForeignKey("achievements.id"))
|
760
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
761
|
+
date_awarded: Mapped[date]
|
762
|
+
|
763
|
+
achievement: Mapped["Achievement"] = relationship(
|
764
|
+
back_populates="achievement_x_user"
|
765
|
+
)
|
766
|
+
user: Mapped["User"] = relationship(back_populates="achievement_x_user")
|
767
|
+
|
768
|
+
|
769
|
+
class Achievement_x_Org(Base):
|
770
|
+
"""
|
771
|
+
Model representing the association between achievements and organizations.
|
772
|
+
|
773
|
+
Attributes:
|
774
|
+
achievement_id (int): The ID of the associated achievement.
|
775
|
+
org_id (int): The ID of the associated organization.
|
776
|
+
achievement (Achievement): The achievement associated with this relationship.
|
777
|
+
org (Org): The organization associated with this relationship.
|
778
|
+
"""
|
779
|
+
|
780
|
+
__tablename__ = "achievements_x_org"
|
781
|
+
__table_args__ = (
|
782
|
+
UniqueConstraint("achievement_id", "org_id", name="_achievement_org_uc"),
|
783
|
+
)
|
784
|
+
|
785
|
+
achievement_id: Mapped[int] = mapped_column(ForeignKey("achievements.id"))
|
786
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
787
|
+
|
788
|
+
achievement: Mapped["Achievement"] = relationship(
|
789
|
+
back_populates="achievement_x_org"
|
790
|
+
)
|
791
|
+
org: Mapped["Org"] = relationship(back_populates="achievement_x_org")
|
792
|
+
|
793
|
+
|
794
|
+
class Position(Base):
|
795
|
+
"""
|
796
|
+
Model representing a position.
|
797
|
+
|
798
|
+
Attributes:
|
799
|
+
name (str): The name of the position.
|
800
|
+
description (Optional[str]): A description of the position.
|
801
|
+
org_type_id (int): The ID of the associated organization type.
|
802
|
+
org_id (int): The ID of the associated organization.
|
803
|
+
"""
|
804
|
+
|
805
|
+
__tablename__ = "positions"
|
806
|
+
|
807
|
+
name: Mapped[str]
|
808
|
+
description: Mapped[Optional[str]]
|
809
|
+
org_type_id: Mapped[int] = mapped_column(ForeignKey("org_types.id"))
|
810
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
811
|
+
|
812
|
+
|
813
|
+
class Position_x_Org_x_User(Base):
|
814
|
+
"""
|
815
|
+
Model representing the association between positions, organizations, and users.
|
816
|
+
|
817
|
+
Attributes:
|
818
|
+
position_id (int): The ID of the associated position.
|
819
|
+
org_id (int): The ID of the associated organization.
|
820
|
+
user_id (int): The ID of the associated user.
|
821
|
+
position (Position): The position associated with this relationship.
|
822
|
+
org (Org): The organization associated with this relationship.
|
823
|
+
user (User): The user associated with this relationship.
|
824
|
+
"""
|
825
|
+
|
826
|
+
__tablename__ = "positions_x_orgs_x_users"
|
827
|
+
__table_args__ = (
|
828
|
+
UniqueConstraint(
|
829
|
+
"position_id", "user_id", "org_id", name="_position_user_org_uc"
|
830
|
+
),
|
831
|
+
)
|
832
|
+
|
833
|
+
position_id: Mapped[int] = mapped_column(ForeignKey("positions.id"))
|
834
|
+
org_id: Mapped[int] = mapped_column(ForeignKey("orgs.id"))
|
835
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
836
|
+
|
837
|
+
position: Mapped["Position"] = relationship(back_populates="position_x_org_x_user")
|
838
|
+
org: Mapped["Org"] = relationship(back_populates="position_x_org_x_user")
|
839
|
+
user: Mapped["User"] = relationship(back_populates="position_x_org_x_user")
|
840
|
+
|
841
|
+
|
842
|
+
class Expansion(Base):
|
843
|
+
"""
|
844
|
+
Model representing an expansion.
|
845
|
+
|
846
|
+
Attributes:
|
847
|
+
area (str): The area of the expansion.
|
848
|
+
pinned_lat (float): The pinned latitude of the expansion.
|
849
|
+
pinned_lon (float): The pinned longitude of the expansion.
|
850
|
+
user_lat (float): The user's latitude.
|
851
|
+
user_lon (float): The user's longitude.
|
852
|
+
interested_in_organizing (bool): Whether the user is interested in organizing.
|
853
|
+
"""
|
854
|
+
|
855
|
+
__tablename__ = "expansions"
|
856
|
+
|
857
|
+
area: Mapped[str]
|
858
|
+
pinned_lat: Mapped[float]
|
859
|
+
pinned_lon: Mapped[float]
|
860
|
+
user_lat: Mapped[float]
|
861
|
+
user_lon: Mapped[float]
|
862
|
+
interested_in_organizing: Mapped[bool]
|
863
|
+
|
864
|
+
|
865
|
+
class Expansion_x_User(Base):
|
866
|
+
"""
|
867
|
+
Model representing the association between expansions and users.
|
868
|
+
|
869
|
+
Attributes:
|
870
|
+
expansion_id (int): The ID of the associated expansion.
|
871
|
+
user_id (int): The ID of the associated user.
|
872
|
+
date (date): The date of the association.
|
873
|
+
notes (Optional[text]): Additional notes for the association.
|
874
|
+
expansion (Expansion): The expansion associated with this relationship.
|
875
|
+
user (User): The user associated with this relationship.
|
876
|
+
"""
|
877
|
+
|
878
|
+
__tablename__ = "expansions_x_users"
|
879
|
+
__table_args__ = (
|
880
|
+
UniqueConstraint("expansion_id", "user_id", name="_expansion_user_uc"),
|
881
|
+
)
|
882
|
+
|
883
|
+
expansion_id: Mapped[int] = mapped_column(ForeignKey("expansions.id"))
|
884
|
+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
885
|
+
date: Mapped[date]
|
886
|
+
notes: Mapped[Optional[text]]
|
887
|
+
|
888
|
+
expansion: Mapped["Expansion"] = relationship(back_populates="expansion_x_user")
|
889
|
+
user: Mapped["User"] = relationship(back_populates="expansion_x_user")
|
f3_data_models/utils.py
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
import os
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import List, Tuple, TypeVar
|
4
|
+
|
5
|
+
import sqlalchemy
|
6
|
+
from sqlalchemy import and_
|
7
|
+
|
8
|
+
from sqlalchemy.dialects.postgresql import insert
|
9
|
+
from sqlalchemy.engine import Engine
|
10
|
+
from sqlalchemy.orm import sessionmaker
|
11
|
+
|
12
|
+
from .models import Base
|
13
|
+
|
14
|
+
from pydot import Dot
|
15
|
+
from sqlalchemy_schemadisplay import create_schema_graph
|
16
|
+
from google.cloud.sql.connector import Connector, IPTypes
|
17
|
+
import pg8000
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class DatabaseField:
|
22
|
+
name: str
|
23
|
+
value: object = None
|
24
|
+
|
25
|
+
|
26
|
+
GLOBAL_ENGINE = None
|
27
|
+
GLOBAL_SESSION = None
|
28
|
+
|
29
|
+
|
30
|
+
def get_engine(echo=False) -> Engine:
|
31
|
+
host = os.environ["DATABASE_HOST"]
|
32
|
+
user = os.environ["DATABASE_USER"]
|
33
|
+
passwd = os.environ["DATABASE_PASSWORD"]
|
34
|
+
database = os.environ["DATABASE_SCHEMA"]
|
35
|
+
|
36
|
+
if os.environ.get("USE_GCP", "False") == "False":
|
37
|
+
db_url = f"postgresql://{user}:{passwd}@{host}:5432/{database}"
|
38
|
+
engine = sqlalchemy.create_engine(db_url, echo=echo)
|
39
|
+
else:
|
40
|
+
connector = Connector()
|
41
|
+
|
42
|
+
def get_connection():
|
43
|
+
conn: pg8000.dbapi.Connection = connector.connect(
|
44
|
+
instance_connection_string=host,
|
45
|
+
driver="pg8000",
|
46
|
+
user=user,
|
47
|
+
password=passwd,
|
48
|
+
db=database,
|
49
|
+
ip_type=IPTypes.PUBLIC,
|
50
|
+
)
|
51
|
+
return conn
|
52
|
+
|
53
|
+
engine = sqlalchemy.create_engine(
|
54
|
+
"postgresql+pg8000://", creator=get_connection, echo=echo
|
55
|
+
)
|
56
|
+
return engine
|
57
|
+
|
58
|
+
|
59
|
+
def get_session(echo=False):
|
60
|
+
if GLOBAL_SESSION:
|
61
|
+
return GLOBAL_SESSION
|
62
|
+
|
63
|
+
global GLOBAL_ENGINE
|
64
|
+
GLOBAL_ENGINE = get_engine(echo=echo)
|
65
|
+
return sessionmaker()(bind=GLOBAL_ENGINE)
|
66
|
+
|
67
|
+
|
68
|
+
def close_session(session):
|
69
|
+
global GLOBAL_SESSION, GLOBAL_ENGINE
|
70
|
+
if GLOBAL_SESSION == session:
|
71
|
+
if GLOBAL_ENGINE:
|
72
|
+
GLOBAL_ENGINE.close()
|
73
|
+
GLOBAL_SESSION = None
|
74
|
+
|
75
|
+
|
76
|
+
T = TypeVar("T")
|
77
|
+
|
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)
|
90
|
+
|
91
|
+
def find_records(cls: T, filters) -> List[T]:
|
92
|
+
session = get_session()
|
93
|
+
try:
|
94
|
+
records = session.query(cls).filter(and_(*filters)).all()
|
95
|
+
for r in records:
|
96
|
+
session.expunge(r)
|
97
|
+
return records
|
98
|
+
finally:
|
99
|
+
session.rollback()
|
100
|
+
close_session(session)
|
101
|
+
|
102
|
+
def find_join_records2(left_cls: T, right_cls: T, filters) -> List[Tuple[T]]:
|
103
|
+
session = get_session()
|
104
|
+
try:
|
105
|
+
records = (
|
106
|
+
session.query(left_cls, right_cls)
|
107
|
+
.join(right_cls)
|
108
|
+
.filter(and_(*filters))
|
109
|
+
.all()
|
110
|
+
)
|
111
|
+
session.expunge_all()
|
112
|
+
return records
|
113
|
+
finally:
|
114
|
+
session.rollback()
|
115
|
+
close_session(session)
|
116
|
+
|
117
|
+
def find_join_records3(
|
118
|
+
left_cls: T, right_cls1: T, right_cls2: T, filters, left_join=False
|
119
|
+
) -> List[Tuple[T]]:
|
120
|
+
session = get_session()
|
121
|
+
try:
|
122
|
+
records = (
|
123
|
+
session.query(left_cls, right_cls1, right_cls2)
|
124
|
+
.select_from(left_cls)
|
125
|
+
.join(right_cls1, isouter=left_join)
|
126
|
+
.join(right_cls2, isouter=left_join)
|
127
|
+
.filter(and_(*filters))
|
128
|
+
.all()
|
129
|
+
)
|
130
|
+
session.expunge_all()
|
131
|
+
return records
|
132
|
+
finally:
|
133
|
+
session.rollback()
|
134
|
+
close_session(session)
|
135
|
+
|
136
|
+
def update_record(cls: T, id, fields):
|
137
|
+
session = get_session()
|
138
|
+
try:
|
139
|
+
session.query(cls).filter(cls.get_id() == id).update(
|
140
|
+
fields, synchronize_session="fetch"
|
141
|
+
)
|
142
|
+
session.flush()
|
143
|
+
finally:
|
144
|
+
session.commit()
|
145
|
+
close_session(session)
|
146
|
+
|
147
|
+
def update_records(cls: T, filters, fields):
|
148
|
+
session = get_session()
|
149
|
+
try:
|
150
|
+
session.query(cls).filter(and_(*filters)).update(
|
151
|
+
fields, synchronize_session="fetch"
|
152
|
+
)
|
153
|
+
session.flush()
|
154
|
+
finally:
|
155
|
+
session.commit()
|
156
|
+
close_session(session)
|
157
|
+
|
158
|
+
def create_record(record: Base) -> Base:
|
159
|
+
session = get_session()
|
160
|
+
try:
|
161
|
+
session.add(record)
|
162
|
+
session.flush()
|
163
|
+
session.expunge(record)
|
164
|
+
finally:
|
165
|
+
session.commit()
|
166
|
+
close_session(session)
|
167
|
+
return record # noqa
|
168
|
+
|
169
|
+
def create_records(records: List[Base]):
|
170
|
+
session = get_session()
|
171
|
+
try:
|
172
|
+
session.add_all(records)
|
173
|
+
session.flush()
|
174
|
+
session.expunge_all()
|
175
|
+
finally:
|
176
|
+
session.commit()
|
177
|
+
close_session(session)
|
178
|
+
return records # noqa
|
179
|
+
|
180
|
+
def create_or_ignore(cls: T, records: List[Base]):
|
181
|
+
session = get_session()
|
182
|
+
try:
|
183
|
+
for record in records:
|
184
|
+
record_dict = {
|
185
|
+
k: v
|
186
|
+
for k, v in record.__dict__.items()
|
187
|
+
if k != "_sa_instance_state"
|
188
|
+
}
|
189
|
+
stmt = insert(cls).values(record_dict).on_conflict_do_nothing()
|
190
|
+
session.execute(stmt)
|
191
|
+
session.flush()
|
192
|
+
finally:
|
193
|
+
session.commit()
|
194
|
+
close_session(session)
|
195
|
+
|
196
|
+
def upsert_records(cls, records):
|
197
|
+
session = get_session()
|
198
|
+
try:
|
199
|
+
for record in records:
|
200
|
+
record_dict = {
|
201
|
+
k: v
|
202
|
+
for k, v in record.__dict__.items()
|
203
|
+
if k != "_sa_instance_state"
|
204
|
+
}
|
205
|
+
stmt = insert(cls).values(record_dict)
|
206
|
+
update_dict = {
|
207
|
+
c.name: getattr(record, c.name) for c in cls.__table__.columns
|
208
|
+
}
|
209
|
+
stmt = stmt.on_conflict_do_update(
|
210
|
+
index_elements=[cls.__table__.primary_key.columns.keys()],
|
211
|
+
set_=update_dict,
|
212
|
+
)
|
213
|
+
session.execute(stmt)
|
214
|
+
session.flush()
|
215
|
+
finally:
|
216
|
+
session.commit()
|
217
|
+
close_session(session)
|
218
|
+
|
219
|
+
def delete_record(cls: T, id):
|
220
|
+
session = get_session()
|
221
|
+
try:
|
222
|
+
session.query(cls).filter(cls.get_id() == id).delete()
|
223
|
+
session.flush()
|
224
|
+
finally:
|
225
|
+
session.commit()
|
226
|
+
close_session(session)
|
227
|
+
|
228
|
+
def delete_records(cls: T, filters):
|
229
|
+
session = get_session()
|
230
|
+
try:
|
231
|
+
session.query(cls).filter(and_(*filters)).delete()
|
232
|
+
session.flush()
|
233
|
+
finally:
|
234
|
+
session.commit()
|
235
|
+
close_session(session)
|
236
|
+
|
237
|
+
def execute_sql_query(sql_query):
|
238
|
+
session = get_session()
|
239
|
+
try:
|
240
|
+
records = session.execute(sql_query)
|
241
|
+
return records
|
242
|
+
finally:
|
243
|
+
close_session(session)
|
244
|
+
|
245
|
+
|
246
|
+
def create_diagram():
|
247
|
+
graph: Dot = create_schema_graph(
|
248
|
+
engine=get_engine(),
|
249
|
+
metadata=Base.metadata,
|
250
|
+
show_datatypes=False,
|
251
|
+
show_indexes=False,
|
252
|
+
rankdir="LR",
|
253
|
+
concentrate=False,
|
254
|
+
)
|
255
|
+
graph.write_png("schema_diagram.png")
|
256
|
+
|
257
|
+
|
258
|
+
if __name__ == "__main__":
|
259
|
+
create_diagram()
|
@@ -0,0 +1,64 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: f3-data-models
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: The data schema and models for F3 Nation applications.
|
5
|
+
Home-page: https://github.com/F3-Nation/f3-data-models
|
6
|
+
License: MIT
|
7
|
+
Author: Evan Petzoldt
|
8
|
+
Author-email: evan.petzoldt@protonmail.com
|
9
|
+
Requires-Python: >=3.12,<4.0
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Requires-Dist: alembic (>=1.14.0,<2.0.0)
|
14
|
+
Requires-Dist: cloud-sql-python-connector (>=1.13.0,<2.0.0)
|
15
|
+
Requires-Dist: graphviz (>=0.20.3,<0.21.0)
|
16
|
+
Requires-Dist: pg8000 (>=1.31.2,<2.0.0)
|
17
|
+
Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0)
|
18
|
+
Requires-Dist: sphinx (>=8.1.3,<9.0.0)
|
19
|
+
Requires-Dist: sphinx-autodoc-typehints (>=2.5.0,<3.0.0)
|
20
|
+
Requires-Dist: sphinx-multiversion (>=0.2.4,<0.3.0)
|
21
|
+
Requires-Dist: sphinx-rtd-theme (>=3.0.2,<4.0.0)
|
22
|
+
Requires-Dist: sqlalchemy (>=2.0.36,<3.0.0)
|
23
|
+
Requires-Dist: sqlalchemy-schemadisplay (>=2.0,<3.0)
|
24
|
+
Project-URL: Documentation, https://github.io/F3-Nation/f3-data-models
|
25
|
+
Project-URL: Repository, https://github.com/F3-Nation/f3-data-models
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
# Overview
|
29
|
+
|
30
|
+
This repository defines the F3 data structure, used by the F3 Slack Bot, Maps, etc. The projected uses SQLAlchemy to define the tables / models.
|
31
|
+
|
32
|
+
# Running Locally
|
33
|
+
|
34
|
+
To load the data structure in your database:
|
35
|
+
|
36
|
+
1. Set up a local db, update `.env.example` and save as `.env`
|
37
|
+
2. Clone the repo, use Poetry to install dependencies:
|
38
|
+
```sh
|
39
|
+
poetry env use 3.12
|
40
|
+
poetry install
|
41
|
+
```
|
42
|
+
3. Run the alembic migration:
|
43
|
+
```sh
|
44
|
+
source .env && poetry run alembic upgrade head
|
45
|
+
```
|
46
|
+
|
47
|
+
# Contributing
|
48
|
+
|
49
|
+
If you would like to make a change, you will need to:
|
50
|
+
|
51
|
+
1. Make the change in `models.py`
|
52
|
+
2. Make a alembic revision:
|
53
|
+
```sh
|
54
|
+
source .env && alembic revision --autogenerate -m "Your Message Here"
|
55
|
+
```
|
56
|
+
3. Make any edits to the migration script in `alembic/versions`
|
57
|
+
4. The github pages documentation will be updated when you push to `main`, but if you would like to preview locally, run:
|
58
|
+
```sh
|
59
|
+
poetry run sphinx-build -b html docs docs/_build/html
|
60
|
+
cd docs
|
61
|
+
poetry run python -m http.server --directory _build/html
|
62
|
+
```
|
63
|
+
> [!TIP]
|
64
|
+
> Adding new fields as nullable (ie `Optional[]`) has the best chance of reducing breaking changes to the apps.
|
@@ -0,0 +1,6 @@
|
|
1
|
+
f3_data_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
f3_data_models/models.py,sha256=TwNym-sXPrbq_2js-liom6Sof60oeJlqa4n09qqoC-E,34015
|
3
|
+
f3_data_models/utils.py,sha256=9XcjAzt21l3IBoeC5E57QglXHxkIAAFbmgh1VJdQFY8,7338
|
4
|
+
f3_data_models-0.1.0.dist-info/METADATA,sha256=51ozDOUHlGtubysMbMqOEuW7YkpGz9ibF8iZSvj5Zyg,2267
|
5
|
+
f3_data_models-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
6
|
+
f3_data_models-0.1.0.dist-info/RECORD,,
|