arize-phoenix 11.23.1__py3-none-any.whl → 12.28.1__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.
Files changed (221) hide show
  1. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
  2. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
  3. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
  4. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
  5. phoenix/__generated__/__init__.py +0 -0
  6. phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
  7. phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
  8. phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
  9. phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
  10. phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
  11. phoenix/__init__.py +2 -1
  12. phoenix/auth.py +27 -2
  13. phoenix/config.py +1594 -81
  14. phoenix/db/README.md +546 -28
  15. phoenix/db/bulk_inserter.py +119 -116
  16. phoenix/db/engines.py +140 -33
  17. phoenix/db/facilitator.py +22 -1
  18. phoenix/db/helpers.py +818 -65
  19. phoenix/db/iam_auth.py +64 -0
  20. phoenix/db/insertion/dataset.py +133 -1
  21. phoenix/db/insertion/document_annotation.py +9 -6
  22. phoenix/db/insertion/evaluation.py +2 -3
  23. phoenix/db/insertion/helpers.py +2 -2
  24. phoenix/db/insertion/session_annotation.py +176 -0
  25. phoenix/db/insertion/span_annotation.py +3 -4
  26. phoenix/db/insertion/trace_annotation.py +3 -4
  27. phoenix/db/insertion/types.py +41 -18
  28. phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
  29. phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
  30. phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
  31. phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
  32. phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
  33. phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
  34. phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
  35. phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
  36. phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
  37. phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
  38. phoenix/db/models.py +364 -56
  39. phoenix/db/pg_config.py +10 -0
  40. phoenix/db/types/trace_retention.py +7 -6
  41. phoenix/experiments/functions.py +69 -19
  42. phoenix/inferences/inferences.py +1 -2
  43. phoenix/server/api/auth.py +9 -0
  44. phoenix/server/api/auth_messages.py +46 -0
  45. phoenix/server/api/context.py +60 -0
  46. phoenix/server/api/dataloaders/__init__.py +36 -0
  47. phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
  48. phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
  49. phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
  50. phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
  51. phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
  52. phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
  53. phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
  54. phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
  55. phoenix/server/api/dataloaders/dataset_labels.py +36 -0
  56. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
  57. phoenix/server/api/dataloaders/document_evaluations.py +6 -9
  58. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
  59. phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
  60. phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
  61. phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
  62. phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
  63. phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
  64. phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
  65. phoenix/server/api/dataloaders/record_counts.py +37 -10
  66. phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
  67. phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
  68. phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
  69. phoenix/server/api/dataloaders/span_costs.py +3 -9
  70. phoenix/server/api/dataloaders/table_fields.py +2 -2
  71. phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
  72. phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
  73. phoenix/server/api/exceptions.py +5 -1
  74. phoenix/server/api/helpers/playground_clients.py +263 -83
  75. phoenix/server/api/helpers/playground_spans.py +2 -1
  76. phoenix/server/api/helpers/playground_users.py +26 -0
  77. phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
  78. phoenix/server/api/helpers/prompts/models.py +61 -19
  79. phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
  80. phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
  81. phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
  82. phoenix/server/api/input_types/DatasetFilter.py +5 -2
  83. phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
  84. phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
  85. phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
  86. phoenix/server/api/input_types/PromptVersionInput.py +47 -1
  87. phoenix/server/api/input_types/SpanSort.py +3 -2
  88. phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
  89. phoenix/server/api/input_types/UserRoleInput.py +1 -0
  90. phoenix/server/api/mutations/__init__.py +8 -0
  91. phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
  92. phoenix/server/api/mutations/api_key_mutations.py +15 -20
  93. phoenix/server/api/mutations/chat_mutations.py +106 -37
  94. phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
  95. phoenix/server/api/mutations/dataset_mutations.py +21 -16
  96. phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
  97. phoenix/server/api/mutations/experiment_mutations.py +2 -2
  98. phoenix/server/api/mutations/export_events_mutations.py +3 -3
  99. phoenix/server/api/mutations/model_mutations.py +11 -9
  100. phoenix/server/api/mutations/project_mutations.py +4 -4
  101. phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
  102. phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
  103. phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
  104. phoenix/server/api/mutations/prompt_mutations.py +65 -129
  105. phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
  106. phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
  107. phoenix/server/api/mutations/trace_annotations_mutations.py +13 -8
  108. phoenix/server/api/mutations/trace_mutations.py +3 -3
  109. phoenix/server/api/mutations/user_mutations.py +55 -26
  110. phoenix/server/api/queries.py +501 -617
  111. phoenix/server/api/routers/__init__.py +2 -2
  112. phoenix/server/api/routers/auth.py +141 -87
  113. phoenix/server/api/routers/ldap.py +229 -0
  114. phoenix/server/api/routers/oauth2.py +349 -101
  115. phoenix/server/api/routers/v1/__init__.py +22 -4
  116. phoenix/server/api/routers/v1/annotation_configs.py +19 -30
  117. phoenix/server/api/routers/v1/annotations.py +455 -13
  118. phoenix/server/api/routers/v1/datasets.py +355 -68
  119. phoenix/server/api/routers/v1/documents.py +142 -0
  120. phoenix/server/api/routers/v1/evaluations.py +20 -28
  121. phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
  122. phoenix/server/api/routers/v1/experiment_runs.py +335 -59
  123. phoenix/server/api/routers/v1/experiments.py +475 -47
  124. phoenix/server/api/routers/v1/projects.py +16 -50
  125. phoenix/server/api/routers/v1/prompts.py +50 -39
  126. phoenix/server/api/routers/v1/sessions.py +108 -0
  127. phoenix/server/api/routers/v1/spans.py +156 -96
  128. phoenix/server/api/routers/v1/traces.py +51 -77
  129. phoenix/server/api/routers/v1/users.py +64 -24
  130. phoenix/server/api/routers/v1/utils.py +3 -7
  131. phoenix/server/api/subscriptions.py +257 -93
  132. phoenix/server/api/types/Annotation.py +90 -23
  133. phoenix/server/api/types/ApiKey.py +13 -17
  134. phoenix/server/api/types/AuthMethod.py +1 -0
  135. phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
  136. phoenix/server/api/types/Dataset.py +199 -72
  137. phoenix/server/api/types/DatasetExample.py +88 -18
  138. phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
  139. phoenix/server/api/types/DatasetLabel.py +57 -0
  140. phoenix/server/api/types/DatasetSplit.py +98 -0
  141. phoenix/server/api/types/DatasetVersion.py +49 -4
  142. phoenix/server/api/types/DocumentAnnotation.py +212 -0
  143. phoenix/server/api/types/Experiment.py +215 -68
  144. phoenix/server/api/types/ExperimentComparison.py +3 -9
  145. phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
  146. phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
  147. phoenix/server/api/types/ExperimentRun.py +120 -70
  148. phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
  149. phoenix/server/api/types/GenerativeModel.py +95 -42
  150. phoenix/server/api/types/GenerativeProvider.py +1 -1
  151. phoenix/server/api/types/ModelInterface.py +7 -2
  152. phoenix/server/api/types/PlaygroundModel.py +12 -2
  153. phoenix/server/api/types/Project.py +218 -185
  154. phoenix/server/api/types/ProjectSession.py +146 -29
  155. phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
  156. phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
  157. phoenix/server/api/types/Prompt.py +119 -39
  158. phoenix/server/api/types/PromptLabel.py +42 -25
  159. phoenix/server/api/types/PromptVersion.py +11 -8
  160. phoenix/server/api/types/PromptVersionTag.py +65 -25
  161. phoenix/server/api/types/Span.py +130 -123
  162. phoenix/server/api/types/SpanAnnotation.py +189 -42
  163. phoenix/server/api/types/SystemApiKey.py +65 -1
  164. phoenix/server/api/types/Trace.py +184 -53
  165. phoenix/server/api/types/TraceAnnotation.py +149 -50
  166. phoenix/server/api/types/User.py +128 -33
  167. phoenix/server/api/types/UserApiKey.py +73 -26
  168. phoenix/server/api/types/node.py +10 -0
  169. phoenix/server/api/types/pagination.py +11 -2
  170. phoenix/server/app.py +154 -36
  171. phoenix/server/authorization.py +5 -4
  172. phoenix/server/bearer_auth.py +13 -5
  173. phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
  174. phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
  175. phoenix/server/daemons/generative_model_store.py +61 -9
  176. phoenix/server/daemons/span_cost_calculator.py +10 -8
  177. phoenix/server/dml_event.py +13 -0
  178. phoenix/server/email/sender.py +29 -2
  179. phoenix/server/grpc_server.py +9 -9
  180. phoenix/server/jwt_store.py +8 -6
  181. phoenix/server/ldap.py +1449 -0
  182. phoenix/server/main.py +9 -3
  183. phoenix/server/oauth2.py +330 -12
  184. phoenix/server/prometheus.py +43 -6
  185. phoenix/server/rate_limiters.py +4 -9
  186. phoenix/server/retention.py +33 -20
  187. phoenix/server/session_filters.py +49 -0
  188. phoenix/server/static/.vite/manifest.json +51 -53
  189. phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
  190. phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
  191. phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
  192. phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
  193. phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
  194. phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
  195. phoenix/server/static/assets/{vendor-recharts-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
  196. phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
  197. phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
  198. phoenix/server/templates/index.html +7 -1
  199. phoenix/server/thread_server.py +1 -2
  200. phoenix/server/utils.py +74 -0
  201. phoenix/session/client.py +55 -1
  202. phoenix/session/data_extractor.py +5 -0
  203. phoenix/session/evaluation.py +8 -4
  204. phoenix/session/session.py +44 -8
  205. phoenix/settings.py +2 -0
  206. phoenix/trace/attributes.py +80 -13
  207. phoenix/trace/dsl/query.py +2 -0
  208. phoenix/trace/projects.py +5 -0
  209. phoenix/utilities/template_formatters.py +1 -1
  210. phoenix/version.py +1 -1
  211. phoenix/server/api/types/Evaluation.py +0 -39
  212. phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
  213. phoenix/server/static/assets/pages-Creyamao.js +0 -8612
  214. phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
  215. phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
  216. phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
  217. phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
  218. phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
  219. phoenix/utilities/deprecation.py +0 -31
  220. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
  221. {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,6 @@ from typing import Optional
3
3
 
4
4
  import strawberry
5
5
  from sqlalchemy import select
6
- from strawberry import Private
7
6
  from strawberry.relay import Node, NodeID
8
7
  from strawberry.types import Info
9
8
 
@@ -12,59 +11,155 @@ from phoenix.db import models
12
11
  from phoenix.server.api.context import Context
13
12
  from phoenix.server.api.exceptions import NotFound
14
13
  from phoenix.server.api.types.AuthMethod import AuthMethod
15
- from phoenix.server.api.types.UserApiKey import UserApiKey, to_gql_api_key
14
+ from phoenix.server.api.types.UserApiKey import UserApiKey
15
+ from phoenix.server.ldap import is_null_email_marker
16
16
 
17
17
  from .UserRole import UserRole, to_gql_user_role
18
18
 
19
19
 
20
20
  @strawberry.type
21
21
  class User(Node):
22
- id_attr: NodeID[int]
23
- password_needs_reset: bool
24
- email: str
25
- username: str
26
- profile_picture_url: Optional[str]
27
- created_at: datetime
28
- user_role_id: Private[int]
29
- auth_method: AuthMethod
22
+ id: NodeID[int]
23
+ db_record: strawberry.Private[Optional[models.User]] = None
24
+
25
+ def __post_init__(self) -> None:
26
+ if self.db_record and self.id != self.db_record.id:
27
+ raise ValueError("User ID mismatch")
28
+
29
+ @strawberry.field
30
+ async def password_needs_reset(
31
+ self,
32
+ info: Info[Context, None],
33
+ ) -> bool:
34
+ if self.db_record:
35
+ val = self.db_record.reset_password
36
+ else:
37
+ val = await info.context.data_loaders.user_fields.load(
38
+ (self.id, models.User.reset_password),
39
+ )
40
+ return val
41
+
42
+ @strawberry.field
43
+ async def email(
44
+ self,
45
+ info: Info[Context, None],
46
+ ) -> str | None:
47
+ if self.db_record:
48
+ val = self.db_record.email
49
+ else:
50
+ val = await info.context.data_loaders.user_fields.load(
51
+ (self.id, models.User.email),
52
+ )
53
+ if is_null_email_marker(val):
54
+ return None
55
+ return val
56
+
57
+ @strawberry.field
58
+ async def username(
59
+ self,
60
+ info: Info[Context, None],
61
+ ) -> str:
62
+ if self.db_record:
63
+ val = self.db_record.username
64
+ else:
65
+ val = await info.context.data_loaders.user_fields.load(
66
+ (self.id, models.User.username),
67
+ )
68
+ return val
69
+
70
+ @strawberry.field
71
+ async def profile_picture_url(
72
+ self,
73
+ info: Info[Context, None],
74
+ ) -> Optional[str]:
75
+ if self.db_record:
76
+ val = self.db_record.profile_picture_url
77
+ else:
78
+ val = await info.context.data_loaders.user_fields.load(
79
+ (self.id, models.User.profile_picture_url),
80
+ )
81
+ return val
82
+
83
+ @strawberry.field
84
+ async def created_at(
85
+ self,
86
+ info: Info[Context, None],
87
+ ) -> datetime:
88
+ if self.db_record:
89
+ val = self.db_record.created_at
90
+ else:
91
+ val = await info.context.data_loaders.user_fields.load(
92
+ (self.id, models.User.created_at),
93
+ )
94
+ return val
95
+
96
+ @strawberry.field
97
+ async def auth_method(
98
+ self,
99
+ info: Info[Context, None],
100
+ ) -> AuthMethod:
101
+ """Return semantic auth method (translated from database for LDAP users).
102
+
103
+ For Approach 1 (zero-migration), LDAP users are stored with auth_method='OAUTH2'
104
+ but have a special Unicode marker in oauth2_client_id. This resolver translates
105
+ that storage convention to the correct semantic AuthMethod.LDAP for the frontend.
106
+ """
107
+ if self.db_record:
108
+ auth_method_val = self.db_record.auth_method
109
+ oauth2_client_id = self.db_record.oauth2_client_id
110
+ else:
111
+ (
112
+ auth_method_val,
113
+ oauth2_client_id,
114
+ ) = await info.context.data_loaders.user_fields.load_many(
115
+ (
116
+ (self.id, models.User.auth_method),
117
+ (self.id, models.User.oauth2_client_id),
118
+ )
119
+ )
120
+
121
+ # Translate LDAP users from database storage to semantic type
122
+ if auth_method_val == "OAUTH2":
123
+ # Import here to avoid circular dependency
124
+ from phoenix.server.ldap import is_ldap_user
125
+
126
+ if is_ldap_user(oauth2_client_id):
127
+ return AuthMethod.LDAP
128
+
129
+ return AuthMethod(auth_method_val)
30
130
 
31
131
  @strawberry.field
32
132
  async def role(self, info: Info[Context, None]) -> UserRole:
33
- role = await info.context.data_loaders.user_roles.load(self.user_role_id)
133
+ if self.db_record:
134
+ user_role_id = self.db_record.user_role_id
135
+ else:
136
+ user_role_id = await info.context.data_loaders.user_fields.load(
137
+ (self.id, models.User.user_role_id),
138
+ )
139
+ role = await info.context.data_loaders.user_roles.load(user_role_id)
34
140
  if role is None:
35
- raise NotFound(f"User role with id {self.user_role_id} not found")
141
+ raise NotFound(f"User role with id {user_role_id} not found")
36
142
  return to_gql_user_role(role)
37
143
 
38
144
  @strawberry.field
39
145
  async def api_keys(self, info: Info[Context, None]) -> list[UserApiKey]:
40
146
  async with info.context.db() as session:
41
147
  api_keys = await session.scalars(
42
- select(models.ApiKey).where(models.ApiKey.user_id == self.id_attr)
148
+ select(models.ApiKey).where(models.ApiKey.user_id == self.id)
43
149
  )
44
- return [to_gql_api_key(api_key) for api_key in api_keys]
150
+ return [UserApiKey(id=api_key.id, db_record=api_key) for api_key in api_keys]
45
151
 
46
152
  @strawberry.field
47
- async def is_management_user(self) -> bool:
153
+ async def is_management_user(self, info: Info[Context, None]) -> bool:
48
154
  initial_admins = get_env_admins()
49
155
  # this field is only visible to initial admins as they are the ones likely to have access to
50
156
  # a management interface / the phoenix environment.
51
- if self.email in initial_admins or self.email == "admin@localhost":
157
+ if self.db_record:
158
+ email = self.db_record.email
159
+ else:
160
+ email = await info.context.data_loaders.user_fields.load(
161
+ (self.id, models.User.email),
162
+ )
163
+ if email in initial_admins or email == "admin@localhost":
52
164
  return True
53
165
  return False
54
-
55
-
56
- def to_gql_user(user: models.User, api_keys: Optional[list[models.ApiKey]] = None) -> User:
57
- """
58
- Converts an ORM user to a GraphQL user.
59
- """
60
- assert user.auth_method is not None
61
- return User(
62
- id_attr=user.id,
63
- password_needs_reset=user.reset_password,
64
- username=user.username,
65
- email=user.email,
66
- profile_picture_url=user.profile_picture_url,
67
- created_at=user.created_at,
68
- user_role_id=user.user_role_id,
69
- auth_method=AuthMethod(user.auth_method),
70
- )
@@ -1,14 +1,13 @@
1
- from typing import TYPE_CHECKING
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, Optional
2
3
 
3
4
  import strawberry
4
- from strawberry import Private
5
5
  from strawberry.relay import Node, NodeID
6
6
  from strawberry.types import Info
7
7
  from typing_extensions import Annotated
8
8
 
9
9
  from phoenix.db.models import ApiKey as OrmApiKey
10
10
  from phoenix.server.api.context import Context
11
- from phoenix.server.api.exceptions import NotFound
12
11
 
13
12
  from .ApiKey import ApiKey
14
13
 
@@ -18,28 +17,76 @@ if TYPE_CHECKING:
18
17
 
19
18
  @strawberry.type
20
19
  class UserApiKey(ApiKey, Node):
21
- id_attr: NodeID[int]
22
- user_id: Private[int]
20
+ id: NodeID[int]
21
+ db_record: strawberry.Private[Optional[OrmApiKey]] = None
22
+
23
+ def __post_init__(self) -> None:
24
+ if self.db_record and self.id != self.db_record.id:
25
+ raise ValueError("UserApiKey ID mismatch")
26
+
27
+ @strawberry.field(description="Name of the API key.") # type: ignore
28
+ async def name(
29
+ self,
30
+ info: Info[Context, None],
31
+ ) -> str:
32
+ if self.db_record:
33
+ val = self.db_record.name
34
+ else:
35
+ val = await info.context.data_loaders.user_api_key_fields.load(
36
+ (self.id, OrmApiKey.name),
37
+ )
38
+ return val
39
+
40
+ @strawberry.field(description="Description of the API key.") # type: ignore
41
+ async def description(
42
+ self,
43
+ info: Info[Context, None],
44
+ ) -> Optional[str]:
45
+ if self.db_record:
46
+ val = self.db_record.description
47
+ else:
48
+ val = await info.context.data_loaders.user_api_key_fields.load(
49
+ (self.id, OrmApiKey.description),
50
+ )
51
+ return val
52
+
53
+ @strawberry.field(description="The date and time the API key was created.") # type: ignore
54
+ async def created_at(
55
+ self,
56
+ info: Info[Context, None],
57
+ ) -> datetime:
58
+ if self.db_record:
59
+ val = self.db_record.created_at
60
+ else:
61
+ val = await info.context.data_loaders.user_api_key_fields.load(
62
+ (self.id, OrmApiKey.created_at),
63
+ )
64
+ return val
65
+
66
+ @strawberry.field(description="The date and time the API key will expire.") # type: ignore
67
+ async def expires_at(
68
+ self,
69
+ info: Info[Context, None],
70
+ ) -> Optional[datetime]:
71
+ if self.db_record:
72
+ val = self.db_record.expires_at
73
+ else:
74
+ val = await info.context.data_loaders.user_api_key_fields.load(
75
+ (self.id, OrmApiKey.expires_at),
76
+ )
77
+ return val
23
78
 
24
79
  @strawberry.field
25
- async def user(self, info: Info[Context, None]) -> Annotated["User", strawberry.lazy(".User")]:
26
- user = await info.context.data_loaders.users.load(self.user_id)
27
- if user is None:
28
- raise NotFound(f"User with id {self.user_id} not found")
29
- from .User import to_gql_user
30
-
31
- return to_gql_user(user)
32
-
33
-
34
- def to_gql_api_key(api_key: OrmApiKey) -> UserApiKey:
35
- """
36
- Converts an ORM API key to a GraphQL UserApiKey type.
37
- """
38
- return UserApiKey(
39
- id_attr=api_key.id,
40
- user_id=api_key.user_id,
41
- name=api_key.name,
42
- description=api_key.description,
43
- created_at=api_key.created_at,
44
- expires_at=api_key.expires_at,
45
- )
80
+ async def user(
81
+ self,
82
+ info: Info[Context, None],
83
+ ) -> Annotated["User", strawberry.lazy(".User")]:
84
+ if self.db_record:
85
+ user_id = self.db_record.user_id
86
+ else:
87
+ user_id = await info.context.data_loaders.user_api_key_fields.load(
88
+ (self.id, OrmApiKey.user_id),
89
+ )
90
+ from .User import User
91
+
92
+ return User(id=user_id)
@@ -1,5 +1,15 @@
1
+ import re
2
+ from base64 import b64decode
3
+
1
4
  from strawberry.relay import GlobalID
2
5
 
6
+ _GLOBAL_ID_PATTERN = re.compile(r"[a-zA-Z]+:[0-9]+")
7
+
8
+
9
+ def is_global_id(node_id: str) -> bool:
10
+ decoded_node_id = b64decode(node_id).decode()
11
+ return _GLOBAL_ID_PATTERN.match(decoded_node_id) is not None
12
+
3
13
 
4
14
  def from_global_id(global_id: GlobalID) -> tuple[str, int]:
5
15
  """
@@ -9,7 +9,7 @@ from strawberry.relay.types import Connection, Edge, NodeType, PageInfo
9
9
  from typing_extensions import TypeAlias, assert_never
10
10
 
11
11
  ID: TypeAlias = int
12
- CursorSortColumnValue: TypeAlias = Union[str, int, float, datetime]
12
+ CursorSortColumnValue: TypeAlias = Union[str, int, float, datetime, None]
13
13
 
14
14
  # A type alias for the connection cursor implementation
15
15
  CursorString = str
@@ -19,6 +19,7 @@ CURSOR_PREFIX = "connection:"
19
19
 
20
20
 
21
21
  class CursorSortColumnDataType(Enum):
22
+ NULL = auto()
22
23
  STRING = auto()
23
24
  INT = auto()
24
25
  FLOAT = auto()
@@ -30,6 +31,10 @@ class CursorSortColumn:
30
31
  type: CursorSortColumnDataType
31
32
  value: CursorSortColumnValue
32
33
 
34
+ def __post_init__(self) -> None:
35
+ if self.value is None:
36
+ self.type = CursorSortColumnDataType.NULL
37
+
33
38
  def __str__(self) -> str:
34
39
  if isinstance(self.value, str):
35
40
  return self.value
@@ -37,7 +42,9 @@ class CursorSortColumn:
37
42
  return str(self.value)
38
43
  if isinstance(self.value, datetime):
39
44
  return self.value.isoformat()
40
- assert_never(self.type)
45
+ if self.value is None:
46
+ return ""
47
+ assert_never(self.value)
41
48
 
42
49
  @classmethod
43
50
  def from_string(cls, type: CursorSortColumnDataType, cursor_string: str) -> "CursorSortColumn":
@@ -50,6 +57,8 @@ class CursorSortColumn:
50
57
  value = float(cursor_string)
51
58
  elif type is CursorSortColumnDataType.DATETIME:
52
59
  value = datetime.fromisoformat(cursor_string)
60
+ elif type is CursorSortColumnDataType.NULL:
61
+ value = None
53
62
  else:
54
63
  assert_never(type)
55
64
  return cls(type=type, value=value)