dataflow-core 2.1.14__py3-none-any.whl → 2.1.15__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 (37) hide show
  1. authenticator/dataflowhubauthenticator.py +29 -44
  2. dataflow/dataflow.py +45 -69
  3. dataflow/environment.py +20 -11
  4. dataflow/models/__init__.py +5 -3
  5. dataflow/models/app_types.py +8 -3
  6. dataflow/models/connection.py +5 -4
  7. dataflow/models/dataflow_zone.py +2 -3
  8. dataflow/models/environment.py +64 -21
  9. dataflow/models/git_ssh.py +2 -1
  10. dataflow/models/org_associations.py +38 -0
  11. dataflow/models/organization.py +78 -0
  12. dataflow/models/pinned_projects.py +2 -2
  13. dataflow/models/project_details.py +9 -6
  14. dataflow/models/recent_project_studio.py +1 -1
  15. dataflow/models/role.py +12 -6
  16. dataflow/models/role_server.py +2 -5
  17. dataflow/models/role_zone.py +8 -3
  18. dataflow/models/server_config.py +9 -5
  19. dataflow/models/team.py +10 -4
  20. dataflow/models/user.py +49 -12
  21. dataflow/models/user_team.py +1 -4
  22. dataflow/models/variables.py +6 -4
  23. dataflow/secrets_manager/factory.py +14 -8
  24. dataflow/secrets_manager/providers/gcp_manager.py +332 -0
  25. dataflow/secrets_manager/service.py +11 -9
  26. dataflow/utils/get_current_user.py +51 -28
  27. {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/METADATA +4 -1
  28. dataflow_core-2.1.15.dist-info/RECORD +63 -0
  29. {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/top_level.txt +1 -0
  30. dfmigration/__init__.py +0 -0
  31. dfmigration/env.py +45 -0
  32. dfmigration/versions/001_initial_baseline_migration.py +20 -0
  33. dfmigration/versions/__init__.py +0 -0
  34. dataflow/models/user_environment.py +0 -16
  35. dataflow_core-2.1.14.dist-info/RECORD +0 -57
  36. {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/WHEEL +0 -0
  37. {dataflow_core-2.1.14.dist-info → dataflow_core-2.1.15.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,332 @@
1
+ import os, base64, json, atexit
2
+ from pathlib import Path
3
+ from google.cloud import secretmanager
4
+ from google.cloud.secretmanager_v1 import SecretManagerServiceClient
5
+ from google.cloud.secretmanager_v1.types import Secret, SecretPayload
6
+ from google.api_core.exceptions import (
7
+ AlreadyExists,
8
+ NotFound,
9
+ PermissionDenied,
10
+ Forbidden,
11
+ ResourceExhausted,
12
+ InvalidArgument,
13
+ FailedPrecondition
14
+ )
15
+ import json
16
+ from ..interface import SecretManager
17
+ from ...utils.exceptions import (
18
+ SecretNotFoundException,
19
+ SecretAlreadyExistsException,
20
+ SecretManagerAuthException,
21
+ SecretManagerServiceException
22
+ )
23
+
24
+ def _setup_gcp_credentials():
25
+ """Setup GCP credentials from base64 encoded JSON environment variable"""
26
+
27
+ # Only run if GOOGLE_APPLICATION_CREDENTIALS is not already set
28
+ if os.getenv('GOOGLE_APPLICATION_CREDENTIALS'):
29
+ return
30
+
31
+ # Get base64 encoded JSON credentials
32
+ encoded_json = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON')
33
+
34
+ if encoded_json:
35
+ try:
36
+ # Decode base64 to JSON string
37
+ json_credentials = base64.b64decode(encoded_json).decode('utf-8')
38
+
39
+ # Validate it's valid JSON
40
+ json.loads(json_credentials) # Just to validate
41
+
42
+ # Create credentials file in home directory
43
+ home_dir = Path.home()
44
+ credentials_dir = home_dir / '.gcp'
45
+ credentials_dir.mkdir(exist_ok=True) # Create .gcp directory if it doesn't exist
46
+
47
+ credentials_path = credentials_dir / 'credentials.json'
48
+
49
+ # Write JSON string to credentials file
50
+ with open(credentials_path, 'w') as f:
51
+ f.write(json_credentials)
52
+
53
+ # Set the standard Google environment variable that the SDK looks for
54
+ os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = str(credentials_path)
55
+
56
+ # Clean up file on exit
57
+ atexit.register(lambda: credentials_path.unlink() if credentials_path.exists() else None)
58
+
59
+ print(f"GCP credentials decoded and configured at {credentials_path}")
60
+
61
+ except Exception as e:
62
+ print(f"Error setting up GCP credentials: {e}")
63
+
64
+
65
+ class GCPSecretsManager(SecretManager):
66
+ """Google Cloud Platform Secrets Manager implementation."""
67
+
68
+ def __init__(self, project_id: str, region: str):
69
+ """Initialize the GCP Secret Manager client.
70
+
71
+ Args:
72
+ project_id: The GCP project ID where secrets will be stored.
73
+ region: The GCP region where secrets will be stored.
74
+ """
75
+ self.project_id = project_id
76
+ self.region = region
77
+ try:
78
+ _setup_gcp_credentials()
79
+ self.client = secretmanager.SecretManagerServiceClient()
80
+ except PermissionDenied as e:
81
+ raise SecretManagerAuthException("initialize_gcp_client", original_error=str(e))
82
+ except Exception as e:
83
+ raise SecretManagerServiceException("initialize_gcp_client", original_error=str(e))
84
+
85
+ def _get_secret_path(self, vault_path: str) -> str:
86
+ """Get the full secret path in GCP format.
87
+
88
+ Args:
89
+ vault_path: The path/name of the secret.
90
+
91
+ Returns:
92
+ The full path to the secret in GCP format.
93
+ """
94
+ return f"projects/{self.project_id}/secrets/{vault_path}"
95
+
96
+ def _get_secret_version_path(self, vault_path: str, version: str = "latest") -> str:
97
+ """Get the full path to a specific secret version.
98
+
99
+ Args:
100
+ vault_path: The path/name of the secret.
101
+ version: The version of the secret (default is "latest").
102
+
103
+ Returns:
104
+ The full path to the secret version in GCP format.
105
+ """
106
+ return f"{self._get_secret_path(vault_path)}/versions/{version}"
107
+
108
+ def create_secret(self, vault_path: str, secret_data: dict) -> str:
109
+ """Create a new secret.
110
+
111
+ Args:
112
+ vault_path: The path/name of the secret.
113
+ region: The region where the secret will be stored.
114
+ secret_data: The data to store in the secret.
115
+
116
+ Returns:
117
+ A success message.
118
+
119
+ Raises:
120
+ SecretAlreadyExistsException: If the secret already exists.
121
+ SecretManagerAuthException: If there are permission issues.
122
+ SecretManagerServiceException: For other service errors.
123
+ """
124
+ try:
125
+ # Convert dictionary to JSON string before saving
126
+ secret_string = json.dumps(secret_data)
127
+
128
+ # First create the secret
129
+ parent = f"projects/{self.project_id}"
130
+ secret = Secret(
131
+ replication={
132
+ "user_managed": {
133
+ "replicas": [
134
+ {"location": self.region }
135
+ ]
136
+ }
137
+ }
138
+ )
139
+
140
+ self.client.create_secret(
141
+ request={
142
+ "parent": parent,
143
+ "secret_id": vault_path,
144
+ "secret": secret
145
+ }
146
+ )
147
+
148
+ # Then add the secret version with the data
149
+ secret_path = self._get_secret_path(vault_path)
150
+ self.client.add_secret_version(
151
+ request={
152
+ "parent": secret_path,
153
+ "payload": {"data": secret_string.encode("UTF-8")}
154
+ }
155
+ )
156
+
157
+ return "Secret created successfully"
158
+ except AlreadyExists as e:
159
+ raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e))
160
+ except PermissionDenied as e:
161
+ raise SecretManagerAuthException("create_secret", original_error=str(e))
162
+ except Forbidden as e:
163
+ raise SecretManagerAuthException("create_secret", original_error=str(e))
164
+ except FailedPrecondition as e:
165
+ # Handle case where secret might be in recovery/deleted state
166
+ if "pending deletion" in str(e).lower() or "scheduled for deletion" in str(e).lower():
167
+ raise SecretAlreadyExistsException("secret", vault_path, original_error=str(e), is_scheduled_for_deletion=True)
168
+ else:
169
+ raise SecretManagerServiceException("create_secret", original_error=str(e))
170
+ except ResourceExhausted as e:
171
+ raise SecretManagerServiceException("create_secret", original_error=str(e))
172
+ except Exception as e:
173
+ raise SecretManagerServiceException("create_secret", original_error=str(e))
174
+
175
+ def get_secret_by_key(self, vault_path: str) -> dict:
176
+ """Get a secret by its key.
177
+
178
+ Args:
179
+ vault_path: The path/name of the secret.
180
+
181
+ Returns:
182
+ The secret data as a dictionary.
183
+
184
+ Raises:
185
+ SecretNotFoundException: If the secret doesn't exist.
186
+ SecretManagerAuthException: If there are permission issues.
187
+ SecretManagerServiceException: For other service errors.
188
+ """
189
+ try:
190
+ # Get the latest version of the secret
191
+ name = self._get_secret_version_path(vault_path)
192
+ response = self.client.access_secret_version(request={"name": name})
193
+
194
+ # Decode the payload and convert JSON string back to dictionary
195
+ secret_string = response.payload.data.decode("UTF-8")
196
+ secret_data = json.loads(secret_string)
197
+ return secret_data
198
+ except NotFound as e:
199
+ raise SecretNotFoundException("secret", vault_path, original_error=str(e))
200
+ except PermissionDenied as e:
201
+ raise SecretManagerAuthException("get_secret", original_error=str(e))
202
+ except Forbidden as e:
203
+ raise SecretManagerAuthException("get_secret", original_error=str(e))
204
+ except InvalidArgument as e:
205
+ raise SecretManagerServiceException("get_secret", original_error=str(e))
206
+ except json.JSONDecodeError as e:
207
+ raise SecretManagerServiceException("get_secret", original_error=str(e))
208
+ except Exception as e:
209
+ raise SecretManagerServiceException("get_secret", original_error=str(e))
210
+
211
+ def update_secret(self, vault_path: str, update_data: dict) -> str:
212
+ """Update an existing secret.
213
+
214
+ Args:
215
+ vault_path: The path/name of the secret.
216
+ update_data: The data to update in the secret.
217
+
218
+ Returns:
219
+ A success message.
220
+
221
+ Raises:
222
+ SecretNotFoundException: If the secret doesn't exist.
223
+ SecretManagerAuthException: If there are permission issues.
224
+ SecretManagerServiceException: For other service errors.
225
+ """
226
+ try:
227
+ # Get current secret data
228
+ current_data = self.get_secret_by_key(vault_path)
229
+
230
+ # Update with new data
231
+ current_data.update(update_data)
232
+
233
+ # Convert updated dictionary to JSON string
234
+ updated_string = json.dumps(current_data)
235
+
236
+ # Add a new version of the secret with updated data
237
+ secret_path = self._get_secret_path(vault_path)
238
+ self.client.add_secret_version(
239
+ request={
240
+ "parent": secret_path,
241
+ "payload": {"data": updated_string.encode("UTF-8")}
242
+ }
243
+ )
244
+
245
+ return "Secret updated successfully"
246
+ except SecretNotFoundException:
247
+ raise
248
+ except PermissionDenied as e:
249
+ raise SecretManagerAuthException("update_secret", original_error=str(e))
250
+ except Forbidden as e:
251
+ raise SecretManagerAuthException("update_secret", original_error=str(e))
252
+ except NotFound as e:
253
+ raise SecretNotFoundException("secret", vault_path, original_error=str(e))
254
+ except InvalidArgument as e:
255
+ raise SecretManagerServiceException("update_secret", original_error=str(e))
256
+ except json.JSONDecodeError as e:
257
+ raise SecretManagerServiceException("update_secret", original_error=str(e))
258
+ except Exception as e:
259
+ raise SecretManagerServiceException("update_secret", original_error=str(e))
260
+
261
+ def delete_secret(self, vault_path: str) -> str:
262
+ """Delete a secret.
263
+
264
+ Args:
265
+ vault_path: The path/name of the secret.
266
+
267
+ Returns:
268
+ A success message.
269
+
270
+ Raises:
271
+ SecretNotFoundException: If the secret doesn't exist.
272
+ SecretManagerAuthException: If there are permission issues.
273
+ SecretManagerServiceException: For other service errors.
274
+ """
275
+ try:
276
+ # Get the full path to the secret
277
+ name = self._get_secret_path(vault_path)
278
+
279
+ # For git-ssh secrets, destroy without recovery
280
+ if "git-ssh" in vault_path:
281
+ # Get all versions to destroy them permanently
282
+ versions = self.client.list_secret_versions(request={"parent": name})
283
+ for version in versions:
284
+ if version.state == secretmanager.SecretVersion.State.ENABLED:
285
+ version_name = f"{name}/versions/{version.name.split('/')[-1]}"
286
+ self.client.destroy_secret_version(request={"name": version_name})
287
+
288
+ # Delete the secret itself
289
+ self.client.delete_secret(request={"name": name})
290
+ else:
291
+ # For regular secrets, use the default 7-day recovery window
292
+ self.client.delete_secret(request={
293
+ "name": name,
294
+ # In GCP, the recovery window is configured at the service level,
295
+ # not per API call, so we don't specify it here
296
+ })
297
+
298
+ return "Secret deleted successfully"
299
+ except NotFound as e:
300
+ raise SecretNotFoundException("secret", vault_path, original_error=str(e))
301
+ except PermissionDenied as e:
302
+ raise SecretManagerAuthException("delete_secret", original_error=str(e))
303
+ except Forbidden as e:
304
+ raise SecretManagerAuthException("delete_secret", original_error=str(e))
305
+ except InvalidArgument as e:
306
+ raise SecretManagerServiceException("delete_secret", original_error=str(e))
307
+ except Exception as e:
308
+ raise SecretManagerServiceException("delete_secret", original_error=str(e))
309
+
310
+ def test_connection(self, vault_path: str) -> str:
311
+ """Test the connection to the secret manager by attempting to access a secret.
312
+
313
+ Args:
314
+ vault_path: The path/name of the secret to test.
315
+
316
+ Returns:
317
+ The status of the secret.
318
+
319
+ Raises:
320
+ SecretNotFoundException: If the secret doesn't exist.
321
+ SecretManagerAuthException: If there are permission issues.
322
+ SecretManagerServiceException: For other service errors.
323
+ """
324
+ try:
325
+ secret = self.get_secret_by_key(vault_path)
326
+ return secret.get('status', 'Unknown')
327
+ except SecretNotFoundException:
328
+ raise
329
+ except (SecretManagerAuthException, SecretManagerServiceException):
330
+ raise
331
+ except Exception as e:
332
+ raise SecretManagerServiceException("test_connection", original_error=str(e))
@@ -117,27 +117,29 @@ class _BaseAccessor(ABC):
117
117
  # These classes implement the logic for building the vault path based on the context.
118
118
  # --------------------------------------------------------------------------
119
119
  class _RuntimeAccessor(_BaseAccessor):
120
- def __init__(self, secret_manager: SecretManager, runtime_env: str, slug: str = None):
120
+ def __init__(self, secret_manager: SecretManager, org_id: str, runtime_env: str, slug: str = None):
121
121
  super().__init__(secret_manager)
122
+ self.org_id = org_id
122
123
  self.runtime_env = runtime_env
123
124
  self.slug = slug
124
125
 
125
126
  def _get_vault_path(self, secret_type: str, key: str) -> str:
126
127
  # Special case for git-ssh in runtime context
127
128
  if secret_type == "git-ssh":
128
- return f"{self.runtime_env}-{secret_type}-{self.slug}"
129
+ return f"{self.org_id}-{self.runtime_env}-{secret_type}-{self.slug}"
129
130
 
130
131
  # Standard format for all other secret types
131
132
  context = self.slug if self.slug else "global"
132
- return f"{self.runtime_env}-{context}-{secret_type}-{key}"
133
+ return f"{self.org_id}-{self.runtime_env}-{context}-{secret_type}-{key}"
133
134
 
134
135
  class _StudioAccessor(_BaseAccessor):
135
- def __init__(self, secret_manager: SecretManager, user_name: str):
136
+ def __init__(self, secret_manager: SecretManager, org_id: str, user_name: str):
136
137
  super().__init__(secret_manager)
138
+ self.org_id = org_id
137
139
  self.user_name = user_name
138
140
 
139
141
  def _get_vault_path(self, secret_type: str, key: str) -> str:
140
- return f"{self.user_name}-{secret_type}-{key}"
142
+ return f"{self.org_id}-{self.user_name}-{secret_type}-{key}"
141
143
 
142
144
  # --------------------------------------------------------------------------
143
145
  # PUBLIC INTERFACE CLASS
@@ -147,10 +149,10 @@ class SecretsService:
147
149
  def __init__(self, secret_manager: SecretManager):
148
150
  self._secret_manager = secret_manager
149
151
 
150
- def runtime(self, env: str, slug: str = None) -> _RuntimeAccessor:
152
+ def runtime(self, org_id: int, env: str, slug: str = None) -> _RuntimeAccessor:
151
153
  """Sets the context to RUNTIME and returns the appropriate accessor."""
152
- return _RuntimeAccessor(self._secret_manager, env, slug)
154
+ return _RuntimeAccessor(self._secret_manager, str(org_id), env, slug)
153
155
 
154
- def studio(self, user: str) -> _StudioAccessor:
156
+ def studio(self, org_id: int, user: str) -> _StudioAccessor:
155
157
  """Sets the context to STUDIO and returns the appropriate accessor."""
156
- return _StudioAccessor(self._secret_manager, user)
158
+ return _StudioAccessor(self._secret_manager, str(org_id), user)
@@ -1,37 +1,60 @@
1
- from fastapi import HTTPException, status
1
+ from fastapi import HTTPException
2
2
  from sqlalchemy.orm import Session
3
- from dataflow.models import user as m_user
4
- from dataflow.models import session as m_session
3
+ from sqlalchemy import and_
4
+ from dataflow.models import (
5
+ user as m_user,
6
+ session as m_session,
7
+ org_associations as m_org_associations,
8
+ role as m_role,
9
+ organization as m_organization
10
+ )
5
11
 
6
12
  def get_user_from_session(session_id: str, db: Session):
7
13
  """
8
- Retrieve a user based on session ID.
9
-
10
- Args:
11
- session_id (str): The unique session identifier
12
- db (Session): Database session
13
-
14
- Returns:
15
- User: User object if found
16
-
17
- Raises:
18
- HTTPException: If session is invalid or user not found
14
+ Retrieve user information based on the session ID.
19
15
  """
20
- session_record = db.query(m_session.Session).filter(m_session.Session.session_id == session_id).first()
21
- if not session_record:
22
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session")
16
+ user = (
17
+ db.query(
18
+ m_user.User,
19
+ m_org_associations.OrganizationUser.role_id,
20
+ m_role.Role.name,
21
+ m_role.Role.base_role,
22
+ m_org_associations.OrganizationUser.active_server_id,
23
+ m_org_associations.OrganizationUser.show_server_page,
24
+ m_org_associations.OrganizationUser.active_env_short_name,
25
+ m_org_associations.OrganizationUser.active_env_type,
26
+ m_org_associations.OrganizationUser.monthly_allocation,
27
+ m_organization.Organization.uid
28
+ )
29
+ .join(m_session.Session, m_session.Session.user_id == m_user.User.user_id)
30
+ .outerjoin(
31
+ m_org_associations.OrganizationUser,
32
+ and_(
33
+ m_org_associations.OrganizationUser.user_id == m_user.User.user_id,
34
+ m_org_associations.OrganizationUser.org_id == m_user.User.active_org_id
35
+ )
36
+ )
37
+ .outerjoin(m_role.Role, m_role.Role.id == m_org_associations.OrganizationUser.role_id)
38
+ .outerjoin(
39
+ m_organization.Organization,
40
+ m_organization.Organization.id == m_user.User.active_org_id # join to Organization
41
+ )
42
+ .filter(m_session.Session.session_id == session_id)
43
+ .first()
44
+ )
23
45
 
24
- user = db.query(m_user.User).filter(m_user.User.user_id == session_record.user_id).first()
25
46
  if not user:
26
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
27
-
28
- base_role = user.role_details.base_role
29
- role_id = user.role_details.id
30
- role_name = user.role_details.name
31
- user.base_role = base_role
32
- user.role = role_name
33
- user.role_id = role_id
34
-
35
- return user
47
+ raise HTTPException(status_code=401, detail="Invalid session")
36
48
 
49
+ user_obj, role_id, role_name, base_role, active_server_id, show_server_page, active_env_short_name, active_env_type, monthly_allocation, uid = user
50
+ user_obj.role_id = role_id
51
+ user_obj.role = role_name
52
+ user_obj.base_role = base_role
53
+ user_obj.current_server_id = active_server_id
54
+ user_obj.show_server_page = show_server_page
55
+ user_obj.active_env = active_env_short_name
56
+ user_obj.active_env_type = active_env_type
57
+ user_obj.monthly_allocation = monthly_allocation
58
+ user_obj.org_uid = str(uid)
37
59
 
60
+ return user_obj
@@ -1,16 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataflow-core
3
- Version: 2.1.14
3
+ Version: 2.1.15
4
4
  Summary: Dataflow core package
5
5
  Author: Dataflow
6
6
  Author-email:
7
7
  Requires-Dist: sqlalchemy
8
+ Requires-Dist: alembic
8
9
  Requires-Dist: boto3
9
10
  Requires-Dist: psycopg2-binary
10
11
  Requires-Dist: pymysql
11
12
  Requires-Dist: requests
12
13
  Requires-Dist: azure-identity
13
14
  Requires-Dist: azure-keyvault-secrets
15
+ Requires-Dist: google-auth
16
+ Requires-Dist: google-cloud-secret-manager
14
17
  Dynamic: author
15
18
  Dynamic: requires-dist
16
19
  Dynamic: summary
@@ -0,0 +1,63 @@
1
+ authenticator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ authenticator/dataflowairflowauthenticator.py,sha256=gEdCiL2yJQ7lYvAwbrjcAkccVMfehoMJldw9eU7cc2s,2243
3
+ authenticator/dataflowhubauthenticator.py,sha256=0dt8-yeFtty1kq50qxBHafzwuWy8fG00QVnq6svH12c,12913
4
+ authenticator/dataflowsupersetauthenticator.py,sha256=NkAmDaIc-ui-qEolu4xz_UY7P_2g8111hwNjPvAOW1Q,2839
5
+ dataflow/__init__.py,sha256=WTRg8HMpMWSgxYJ9ZGVldx4k07fAbta3mBmZ1hG9mWE,30
6
+ dataflow/configuration.py,sha256=7To6XwH1eESiYp39eqPcswXWwrdBUdPF6xN6WnazOF0,663
7
+ dataflow/database_manager.py,sha256=tJHMuOZ9Muskrh9t4uLRlTuFU0VkHAzoHlGP5DORIC4,899
8
+ dataflow/dataflow.py,sha256=EzeOMtMCqG0WJ0vld6ArKbKNFAmA9sbjBqVJ8sd2sQw,13026
9
+ dataflow/db.py,sha256=73ojGqpCTRVTlPszD73Ozhjih_BI2KTHmazqxxL6iWk,3780
10
+ dataflow/environment.py,sha256=7XiSojSIOnMOz5YVk-k6w1yQCKbBTTvUxv5cnn6cqJg,28830
11
+ dataflow/models/__init__.py,sha256=FHus-b2oTgLSJ9H7dOCNJ14XpjTzHARqK_Vwh8VhCtM,1178
12
+ dataflow/models/app_types.py,sha256=BNGtjwpQU6MSpBGp0pDzUI4uDZNqvE3_x33itIInasM,689
13
+ dataflow/models/blacklist_library.py,sha256=B2oi3Z8GcR_glhLAyinFk0W8c9txXvm3uOER6dY-q7I,991
14
+ dataflow/models/connection.py,sha256=c1zfwanuZRf6NvtA7lVU8z3taj_QX-bQSO1zbQzcBI0,1174
15
+ dataflow/models/dataflow_zone.py,sha256=PHvZRpINTBN3NTxHYsSuTR-v2G2jZHF0zIIIrQVaBJw,762
16
+ dataflow/models/environment.py,sha256=vkuFBRY9pZNMRXx3F4ZI6P2Nr-UNpqwzZkT-gM4NhyU,5097
17
+ dataflow/models/environment_status.py,sha256=lvPDNUsUoTW9D97B07aKqJQHRKp4LvPM28pQDMPH1ac,536
18
+ dataflow/models/git_ssh.py,sha256=xRkzK8lZ4Fa25zhE3Y9J78BiWv6xtd10YDBNf1XDKXE,797
19
+ dataflow/models/org_associations.py,sha256=iih8xpbcWTsvOdBTs9LjW8kqPtFy7sBhIh7IU2pMGvo,1991
20
+ dataflow/models/organization.py,sha256=hrvwQQuypM20eFX48QPmly1N0SM_pgML38BYA98vMb8,3446
21
+ dataflow/models/pinned_projects.py,sha256=R_Euj98J2LzWrmedQ-A7mAluDyWes5VWt5dgqUs7EWc,639
22
+ dataflow/models/pod_activity.py,sha256=4NQplUtckS4d2Uc-Iyi8PfgciblHiExbXo_VTMaukB8,751
23
+ dataflow/models/pod_session_history.py,sha256=-O8DHtH1AtGLntJ1shI6eiciTJGL_vYvG3gfupvr0zY,778
24
+ dataflow/models/project_details.py,sha256=C7b2VTOiF1BiUfgixIxdyZ2fMncCR7klQdTpF1rB6Wc,1342
25
+ dataflow/models/recent_project_studio.py,sha256=5E5f55FXOq7vC76GNw-Bf8dUKmpFjwkkD-qthPGFKhM,836
26
+ dataflow/models/recent_projects.py,sha256=OFd5MSRXVRHs9UbvUNoJBBnh9rgsJ0lwE23wm5_Hc5w,321
27
+ dataflow/models/role.py,sha256=Kw9zqpG3R5nzwPXzg8MaoJGwDzN9r0rrOAKcVpsdkeY,1307
28
+ dataflow/models/role_server.py,sha256=Qz9VqGSpVGvfLz9_lKITUfkiuR7pmVc8SIIfYrDPDso,547
29
+ dataflow/models/role_zone.py,sha256=kQ9tUxxpwQ_VdbUNKqyacQ87SovsKKP5K6-EFrSTphk,962
30
+ dataflow/models/server_config.py,sha256=FHO4y0Lm7wBcYDQYn1144duOkihIdRTLPIK0sfDEXIo,1768
31
+ dataflow/models/session.py,sha256=c8TI6qXsM8utzp5vSQtAOXJSbgasnyu-a0qSAvA-rWs,459
32
+ dataflow/models/team.py,sha256=u5VKOxi-0wGgxssIhXr1Ydn3X9IEAHNWxz029KaVm5g,780
33
+ dataflow/models/user.py,sha256=ggJftOVLBEs_6fhn1_13Joe9aL32KMnJwHfBMzeMAOA,2959
34
+ dataflow/models/user_team.py,sha256=vD6BsiMfx1iIMQiT8fpwhsQ8AGuPuBO83F22Cy7j-nU,524
35
+ dataflow/models/variables.py,sha256=qCyiVbzC3Wd2onLx-eCY36VWiVytxxWvYld6_3ki5H8,1572
36
+ dataflow/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ dataflow/schemas/connection.py,sha256=ut2sqz06yOjmFKzHry92FEt7DN09Bj30GYse35__Cuw,2467
38
+ dataflow/schemas/git_ssh.py,sha256=N1O7HM6ZbygIBZn2rKvNR0e7IM3ZJMAH6aJtjaghDr0,1283
39
+ dataflow/schemas/secret.py,sha256=wMSCn6zoBHS-R4NoKwljq2JUad8p9JY542UNJFa86X8,1183
40
+ dataflow/scripts/clone_environment.sh,sha256=Qy0GylsA3kUVUL_L1MirxIWujOFhT1tikKqXNtCTWd4,506
41
+ dataflow/scripts/create_environment.sh,sha256=3FHgNplJuEZvyTsLqlCJNX9oyfXgsfqn80VZk2xtvso,828
42
+ dataflow/scripts/update_environment.sh,sha256=2dtn2xlNi6frpig-sqlGE1_IKRbbkqYOCpf_qyMKKII,992
43
+ dataflow/secrets_manager/__init__.py,sha256=idGqIDtYl0De2WIK9Obl-N7SDPSYtVM0D-wXfZjCiy4,559
44
+ dataflow/secrets_manager/factory.py,sha256=LblshkGG9q2C3RHYp0QykianUtpOOQz7sBdlerutyWY,2479
45
+ dataflow/secrets_manager/interface.py,sha256=HhrKpQrprWIbDsVfU_qc59OXmSIuHXv106OXv6-Epqc,506
46
+ dataflow/secrets_manager/service.py,sha256=A4PgiSS1bzlzXPi_GvsiepChULqAa5yONGkaU-d5MLM,7419
47
+ dataflow/secrets_manager/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ dataflow/secrets_manager/providers/aws_manager.py,sha256=16peXyKeuAjv2RVTMUjrzArPYENK9Zu7jREWVgMfScA,8671
49
+ dataflow/secrets_manager/providers/azure_manager.py,sha256=sWOz-7ALnLt6vyM3lt14GBpzpmDnlH3hkdqtuApqkgU,9430
50
+ dataflow/secrets_manager/providers/gcp_manager.py,sha256=AJgotHZRQraxtmfJX1Z8u2Gcr7KJLRJTN_qbth3A5Xk,13738
51
+ dataflow/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ dataflow/utils/exceptions.py,sha256=8GRFoYZ5dPGQckVm2znaHpPi0ZAs69fK-RGKukEsapk,4432
53
+ dataflow/utils/get_current_user.py,sha256=iPyUpEpuxitdmFwvaPiRWIlII8JaaGoxUhQcmNWi0vI,2319
54
+ dataflow/utils/logger.py,sha256=7BFrOq5Oiqn8P4XZbgJzMP5O07d2fpdECbbfsjrUuHw,1213
55
+ dfmigration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ dfmigration/env.py,sha256=w_3Kzr3LJCU5l1zBGsNcoNGZyR8fZEtMRsxE-cEqfHQ,1110
57
+ dfmigration/versions/001_initial_baseline_migration.py,sha256=lxjs7LZLs9-c7FQj7t7t49EOfLPPHMLI6iNp0PIBMRA,299
58
+ dfmigration/versions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
+ dataflow_core-2.1.15.dist-info/METADATA,sha256=Urk3FJfV79VaX794Lgc-q1d-2JvOhUQnWGzwmElyUpQ,463
60
+ dataflow_core-2.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
61
+ dataflow_core-2.1.15.dist-info/entry_points.txt,sha256=ppj_EIbYrJJwCPg1kfdsZk5q1N-Ejfis1neYrnjhO8o,117
62
+ dataflow_core-2.1.15.dist-info/top_level.txt,sha256=-gGoIBh-bUMtzOdna2jsDOdkE2CRu3w3aA7yEFCmNAI,35
63
+ dataflow_core-2.1.15.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  authenticator
2
2
  dataflow
3
+ dfmigration
File without changes
dfmigration/env.py ADDED
@@ -0,0 +1,45 @@
1
+ from sqlalchemy import engine_from_config, pool
2
+ from alembic import context
3
+ import os
4
+
5
+ config = context.config
6
+ target_metadata = None
7
+
8
+ def get_url():
9
+ return os.getenv('DATABASE_URL')
10
+
11
+ def run_migrations_offline() -> None:
12
+ url = get_url()
13
+ context.configure(
14
+ url=url,
15
+ target_metadata=target_metadata,
16
+ literal_binds=True,
17
+ dialect_opts={"paramstyle": "named"},
18
+ )
19
+
20
+ with context.begin_transaction():
21
+ context.run_migrations()
22
+
23
+ def run_migrations_online() -> None:
24
+ configuration = config.get_section(config.config_ini_section) or {}
25
+ configuration['sqlalchemy.url'] = get_url()
26
+
27
+ connectable = engine_from_config(
28
+ configuration,
29
+ prefix="sqlalchemy.",
30
+ poolclass=pool.NullPool,
31
+ )
32
+
33
+ with connectable.connect() as connection:
34
+ context.configure(
35
+ connection=connection,
36
+ target_metadata=target_metadata
37
+ )
38
+
39
+ with context.begin_transaction():
40
+ context.run_migrations()
41
+
42
+ if context.is_offline_mode():
43
+ run_migrations_offline()
44
+ else:
45
+ run_migrations_online()
@@ -0,0 +1,20 @@
1
+ """Initial baseline migration
2
+
3
+ Revision ID: 001
4
+ Revises: None
5
+ Create Date: 2025-09-10 15:00:00.000000
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+ revision = '001'
12
+ down_revision = None
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+ def upgrade() -> None:
17
+ pass
18
+
19
+ def downgrade() -> None:
20
+ pass
File without changes
@@ -1,16 +0,0 @@
1
- from sqlalchemy import Column, Integer, String, DateTime
2
- from sqlalchemy.sql.schema import ForeignKey
3
- from sqlalchemy.sql.expression import func
4
- from dataflow.db import Base
5
-
6
- class UserEnvironment(Base):
7
- """
8
- Table USER_ENVIRONMENT
9
- """
10
-
11
- __tablename__ = 'USER_ENVIRONMENT'
12
-
13
- id = Column(Integer, primary_key=True, index=True, autoincrement=True)
14
- user_id = Column(Integer, ForeignKey('USER.user_id', ondelete="CASCADE"), nullable=False)
15
- env_name = Column(String)
16
- timestamp = Column(DateTime, server_default=func.now(), onupdate=func.now())