dataflow-core 2.1.30rc12__tar.gz → 2.1.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/PKG-INFO +4 -2
  2. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/environment.py +229 -73
  3. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/__init__.py +1 -0
  4. dataflow_core-2.1.32/dataflow/models/dbml_diagram.py +58 -0
  5. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/environment.py +22 -1
  6. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/project_details.py +16 -1
  7. dataflow_core-2.1.32/dataflow/scripts/build_from_upper.sh +103 -0
  8. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/scripts/clone_environment.sh +1 -1
  9. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/scripts/create_environment.sh +1 -1
  10. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/PKG-INFO +4 -2
  11. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/SOURCES.txt +5 -0
  12. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/requires.txt +3 -1
  13. dataflow_core-2.1.32/dfmigration/versions/027_local_environment_workset_columns.py +46 -0
  14. dataflow_core-2.1.32/dfmigration/versions/028_create_diagram_table.py +60 -0
  15. dataflow_core-2.1.32/dfmigration/versions/029_project_studio_source.py +40 -0
  16. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/setup.py +4 -2
  17. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/README.md +0 -0
  18. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/__init__.py +0 -0
  19. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/dataflowairflowauthenticator.py +0 -0
  20. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/dataflowhubauthenticator.py +0 -0
  21. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/dataflowsupersetauthenticator.py +0 -0
  22. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/utils/__init__.py +0 -0
  23. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/utils/verify_email_domain.py +0 -0
  24. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/__init__.py +0 -0
  25. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/configuration.py +0 -0
  26. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/database_manager.py +0 -0
  27. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/dataflow.py +0 -0
  28. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/db.py +0 -0
  29. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/active_server_view.py +0 -0
  30. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/ai_provider.py +0 -0
  31. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/app_types.py +0 -0
  32. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/blacklist_library.py +0 -0
  33. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/connection.py +0 -0
  34. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/connection_type.py +0 -0
  35. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/container.py +0 -0
  36. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/container_session_history.py +0 -0
  37. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/dataflow_setting.py +0 -0
  38. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/dataflow_zone.py +0 -0
  39. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/environment_status.py +0 -0
  40. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/git_ssh.py +0 -0
  41. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/inference_service.py +0 -0
  42. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/onboarding_project.py +0 -0
  43. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/org_associations.py +0 -0
  44. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/organization.py +0 -0
  45. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/otp.py +0 -0
  46. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/pinned_projects.py +0 -0
  47. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/pod_activity.py +0 -0
  48. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/pod_session_history.py +0 -0
  49. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/recent_project_studio.py +0 -0
  50. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/recent_projects.py +0 -0
  51. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/role.py +0 -0
  52. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/role_server.py +0 -0
  53. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/role_zone.py +0 -0
  54. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/server_config.py +0 -0
  55. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/session.py +0 -0
  56. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/subscription.py +0 -0
  57. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/team.py +0 -0
  58. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/user.py +0 -0
  59. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/user_team.py +0 -0
  60. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/variables.py +0 -0
  61. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/__init__.py +0 -0
  62. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/ai_api_key.py +0 -0
  63. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/connection.py +0 -0
  64. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/git_ssh.py +0 -0
  65. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/secret.py +0 -0
  66. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/__init__.py +0 -0
  67. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/factory.py +0 -0
  68. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/interface.py +0 -0
  69. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/__init__.py +0 -0
  70. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/aws_manager.py +0 -0
  71. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/azure_manager.py +0 -0
  72. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/gcp_manager.py +0 -0
  73. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/infisical_manager.py +0 -0
  74. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/ovh_manager.py +0 -0
  75. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/service.py +0 -0
  76. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/utils.py +0 -0
  77. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/__init__.py +0 -0
  78. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/blocked_domains.py +0 -0
  79. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/exceptions.py +0 -0
  80. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/get_current_user.py +0 -0
  81. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/local_env_validation.py +0 -0
  82. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/logger.py +0 -0
  83. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/dependency_links.txt +0 -0
  84. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/entry_points.txt +0 -0
  85. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/top_level.txt +0 -0
  86. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/__init__.py +0 -0
  87. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/env.py +0 -0
  88. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/001_initial_baseline_migration.py +0 -0
  89. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/002_user_onboarding_migration.py +0 -0
  90. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/003_runtime_visibility.py +0 -0
  91. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/004_runtime_custom_image.py +0 -0
  92. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/005_session_table_migration.py +0 -0
  93. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/006_super_admin_migration.py +0 -0
  94. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/007_show_walkthrough_and_org_email_migration.py +0 -0
  95. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/008_costing_migration.py +0 -0
  96. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/009_session_migration.py +0 -0
  97. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/010_add_environment_job_table.py +0 -0
  98. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/011_connection_type_migration.py +0 -0
  99. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/012_app_type_public_and_project_description.py +0 -0
  100. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/013_add_container_table.py +0 -0
  101. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/014_active_server_view_can_spawn.py +0 -0
  102. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/015_add_agent_enabled_org.py +0 -0
  103. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/016_org_server_visibility_controls.py +0 -0
  104. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/017_add_created_at_to_user.py +0 -0
  105. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/018_add_inference_service_table.py +0 -0
  106. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/019_user_onboarding_demo_selection.py +0 -0
  107. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/020_create_onboarding_project_table.py +0 -0
  108. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/021_create_ai_byok_tables.py +0 -0
  109. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/022_shared_airflow_tenancy.py +0 -0
  110. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/023_session_history_table_update_container_table.py +0 -0
  111. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/024_add_status_reason_to_local_environment.py +0 -0
  112. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/025_add_project_preview_image.py +0 -0
  113. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/026_runtime_costing.py +0 -0
  114. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/__init__.py +0 -0
  115. {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/setup.cfg +0 -0
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataflow-core
3
- Version: 2.1.30rc12
3
+ Version: 2.1.32
4
4
  Summary: Dataflow core package
5
5
  Author: Dataflow
6
6
  Author-email:
7
- Requires-Dist: sqlalchemy
7
+ Requires-Dist: sqlalchemy<2.1
8
8
  Requires-Dist: alembic
9
9
  Requires-Dist: boto3
10
10
  Requires-Dist: psycopg2-binary
11
11
  Requires-Dist: pymysql
12
+ Requires-Dist: pymssql<3,>=2.2.8
13
+ Requires-Dist: sqlalchemy-bigquery<2,>=1.12.1
12
14
  Requires-Dist: requests
13
15
  Requires-Dist: azure-identity
14
16
  Requires-Dist: azure-keyvault-secrets
@@ -1,5 +1,5 @@
1
1
  import os, shutil, subprocess, datetime, yaml, re
2
- from .models.environment import JobLogs, Environment, LocalEnvironment
2
+ from .models.environment import JobLogs, Environment, LocalEnvironment, ArchivedEnvironment
3
3
  import json, asyncio, pkg_resources
4
4
  from sqlalchemy.orm import Session
5
5
  from .configuration import ConfigurationManager
@@ -74,7 +74,11 @@ class EnvironmentManager:
74
74
  if db and log_file_location:
75
75
  log_file_name = os.path.basename(log_file_location)
76
76
  await self._update_job_status(log_file_name, build_status, log_file_location, db)
77
- pip_libraries, conda_libraries = self.update_library_versions(yaml_path)
77
+ if build_status == "success":
78
+ env_path = os.path.join(self.env_build_path, env_name)
79
+ pip_libraries, conda_libraries = self.update_library_versions(env_path)
80
+ else:
81
+ pip_libraries, conda_libraries = None, None
78
82
  self.update_environment_db(env_name, env_version, pip_libraries, conda_libraries, build_status, db)
79
83
 
80
84
  return build_status
@@ -164,7 +168,13 @@ class EnvironmentManager:
164
168
  os.symlink(old_yaml_path, new_yaml_path)
165
169
  os.symlink(old_squashfs_path, new_squashfs_path)
166
170
 
167
- pip_libraries, conda_libraries = self.update_library_versions(new_yaml_path)
171
+ archived = db.query(ArchivedEnvironment).filter(
172
+ ArchivedEnvironment.short_name == env_name,
173
+ ArchivedEnvironment.version == str(revert_version),
174
+ ArchivedEnvironment.org_id == self.org_id
175
+ ).first()
176
+ pip_libraries = archived.pip_libraries if archived else None
177
+ conda_libraries = archived.conda_libraries if archived else None
168
178
 
169
179
  self.update_environment_db(env_name, new_version, pip_libraries, conda_libraries, "success", db)
170
180
  return "success"
@@ -172,30 +182,180 @@ class EnvironmentManager:
172
182
  except Exception as e:
173
183
  self.logger.critical(f"Failed to revert environment {env_name}: {e}", exc_info=True, extra={"status_code": 500, "environment": env_name})
174
184
  return "failed"
175
-
185
+
186
+ async def build_env_from_upper(self, env_name, base_short_name, base_version, base_created_by,
187
+ tar_path, py_version, env_version, user_name=None, db: Session = None):
188
+ """Builds a new squashfs environment from a captured overlay upperdir.
189
+
190
+ Reconstructs the overlay a user's activated environment ran with at
191
+ runtime (the base environment's squashfs as the read-only lowerdir,
192
+ plus a tar of the writable upperdir capturing their pip/conda
193
+ modifications) and repackages the merged result as a new versioned
194
+ squashfs. This is the "Save as Draft" build path -- as opposed to
195
+ create_env, which builds a fresh conda environment from a YAML spec.
196
+
197
+ A conda env bakes its absolute prefix into shebangs, conda-meta and
198
+ .pth files, so build_from_upper.sh branches on whether the prefix
199
+ changes: same prefix ("Save") squashes the merged tree verbatim, a new
200
+ prefix ("Save As" under a new short name) clones it first so conda
201
+ rewrites those paths.
202
+
203
+ Args:
204
+ env_name (str): Short name for the new/target environment
205
+ base_short_name (str): Short name of the activated (base) environment
206
+ base_version (str): Version of the activated (base) environment
207
+ base_created_by (str): created_by of the base environment ("Dataflow"
208
+ for system environments, which live under the shared "dataflow"
209
+ sub path instead of an org-scoped one)
210
+ tar_path (str): Path to the tarred overlay upperdir
211
+ py_version (str): Python version, recorded in the conda YAML
212
+ env_version (str): Version to assign to the new environment
213
+ user_name (str): Username who initiated the save
214
+ db (Session): Database session
215
+
216
+ Returns:
217
+ str: Build status ('success' or 'failed')
218
+ """
219
+ log_file_location = None
220
+ if db:
221
+ log_file_location = self._setup_logging(env_name, env_version, user_name, db)
222
+
223
+ env_root_path = self.config.get_config_value('paths', 'env_path')
224
+ env_mount_root = self.config.get_config_value('paths', 'env_mount_path')
225
+ if base_created_by == "Dataflow":
226
+ base_squashfs_path = os.path.join(env_root_path, "dataflow", "python_envs", f"{base_short_name}_v{base_version}.squashfs")
227
+ base_sub_path = "dataflow"
228
+ else:
229
+ base_squashfs_path = os.path.join(self.env_base_path, f"{base_short_name}_v{base_version}.squashfs")
230
+ base_sub_path = self.env_sub_path
231
+
232
+ # Prefixes must match what dataflow-init mounts the squashfs at, since
233
+ # that is the path baked into the environment's own files:
234
+ # <env_mount_path>/{dataflow|org_id}/<short_name>.
235
+ base_prefix = os.path.join(env_mount_root, base_sub_path, base_short_name)
236
+ target_prefix = os.path.join(self.env_build_path, env_name)
237
+
238
+ yaml_path = os.path.join(self.env_version_path, f"{env_name}_v{env_version}.yaml")
239
+ scratch_dir = os.path.join(self.env_build_path, f"{env_name}_v{env_version}_scratch")
240
+
241
+ build_status = await self._execute_env_operation(
242
+ env_name=env_name,
243
+ status="draft",
244
+ mode="from_upper",
245
+ yaml_file_path=yaml_path,
246
+ log_file_location=log_file_location,
247
+ env_version=int(env_version),
248
+ py_version=py_version,
249
+ base_squashfs_path=base_squashfs_path,
250
+ tar_path=tar_path,
251
+ scratch_dir=scratch_dir,
252
+ base_prefix=base_prefix,
253
+ target_prefix=target_prefix,
254
+ )
255
+
256
+ # Update job log status and library versions if db was provided
257
+ if db and log_file_location:
258
+ log_file_name = os.path.basename(log_file_location)
259
+ await self._update_job_status(log_file_name, build_status, log_file_location, db)
260
+
261
+ # build_from_upper.sh deliberately leaves the overlay mounted (and
262
+ # any clone in place) on success so we can introspect the built
263
+ # environment here before tearing it down -- unlike create_env's
264
+ # build prefix, base_prefix is a mount, not a plain directory, so
265
+ # it must be read before cleanup. On failure nothing was left
266
+ # behind (or it was torn down by the script's own failure trap),
267
+ # so the path is just an empty directory -- reading it would
268
+ # silently report zero packages and blank out this version's known
269
+ # library list. Leave pip_libraries/conda_libraries as None
270
+ # (update_environment_db then skips those columns) so a failed
271
+ # build doesn't clobber whatever the row already had.
272
+ if build_status == "success":
273
+ pip_libraries, conda_libraries = self.update_library_versions(target_prefix)
274
+ self.create_conda_yaml(
275
+ yaml_path=yaml_path,
276
+ env_name=env_name,
277
+ python_version=py_version,
278
+ conda_packages=conda_libraries,
279
+ pip_packages=pip_libraries,
280
+ )
281
+ else:
282
+ pip_libraries, conda_libraries = None, None
283
+ self.update_environment_db(env_name, env_version, pip_libraries, conda_libraries, build_status, db)
284
+
285
+ self._cleanup_overlay_scratch(scratch_dir, base_prefix, target_prefix)
286
+
287
+ # The tar is a one-shot input; remove it regardless of outcome so a
288
+ # retry doesn't silently reuse a stale/partial capture, and so it
289
+ # doesn't linger on the user's NFS home.
290
+ try:
291
+ if os.path.exists(tar_path):
292
+ os.remove(tar_path)
293
+ except OSError as e:
294
+ self.logger.error(f"Failed to remove upper-layer tar {tar_path}: {e}")
295
+
296
+ return build_status
297
+
298
+ def _cleanup_overlay_scratch(self, scratch_dir: str, base_prefix: str, target_prefix: str):
299
+ """Unmounts the overlay left mounted by build_from_upper.sh and removes
300
+ its scratch directory, plus the clone it may have built at
301
+ target_prefix. Safe to call even if nothing ended up mounted (e.g. the
302
+ build failed before the overlay mount step).
303
+
304
+ Args:
305
+ scratch_dir (str): The lower/upper/work scratch directory passed to
306
+ build_from_upper.sh
307
+ base_prefix (str): The base environment's prefix, where the merged
308
+ overlay was mounted
309
+ target_prefix (str): The new environment's prefix; equal to
310
+ base_prefix on a version bump, otherwise the cloned env
311
+ """
312
+ lower_dir = os.path.join(scratch_dir, "lower")
313
+ for mount_dir in (base_prefix, lower_dir):
314
+ try:
315
+ if subprocess.run(["mountpoint", "-q", mount_dir]).returncode == 0:
316
+ subprocess.run(["umount", mount_dir], check=True)
317
+ except Exception as e:
318
+ self.logger.error(f"Failed to unmount {mount_dir}: {e}")
319
+ shutil.rmtree(scratch_dir, ignore_errors=True)
320
+ shutil.rmtree(target_prefix, ignore_errors=True)
321
+
176
322
  async def _execute_env_operation(
177
- self,
178
- env_name: str,
179
- status: str,
180
- mode: str,
181
- yaml_file_path: str,
323
+ self,
324
+ env_name: str,
325
+ status: str,
326
+ mode: str,
327
+ yaml_file_path: str,
182
328
  env_version: int,
183
- source_path=None,
329
+ source_path=None,
184
330
  log_file_location=None,
185
- py_version=None
331
+ py_version=None,
332
+ base_squashfs_path=None,
333
+ tar_path=None,
334
+ scratch_dir=None,
335
+ base_prefix=None,
336
+ target_prefix=None,
186
337
  ):
187
- """Executes environment operations (create or clone).
188
-
338
+ """Executes environment operations (create, clone, or from_upper).
339
+
189
340
  Args:
190
341
  env_name (str): Name of the environment
191
342
  status (str): Environment status ('draft' or 'published')
192
- mode (str): Operation mode ('create' or 'clone')
343
+ mode (str): Operation mode ('create', 'clone', 'local_clone', or 'from_upper')
193
344
  env_version (str): Version of the environment (for draft environments)
194
345
  py_version (str): Python version to use (for create mode)
195
346
  py_requirements (list): List of packages to install (for create mode)
196
347
  source_path (str): Path to source environment (for clone mode)
197
348
  log_file_location (str): Path to log file
198
-
349
+ base_squashfs_path (str): Path to the base squashfs to loop-mount as the
350
+ overlay lowerdir (for from_upper mode)
351
+ tar_path (str): Path to the tarred overlay upperdir (for from_upper mode)
352
+ scratch_dir (str): Scratch directory for the lower/upper/work
353
+ mounts (for from_upper mode)
354
+ base_prefix (str): Prefix the base environment was built at, where
355
+ the merged overlay gets mounted (for from_upper mode)
356
+ target_prefix (str): Prefix the new environment will be mounted at;
357
+ differing from base_prefix triggers the clone (for from_upper mode)
358
+
199
359
  Returns:
200
360
  str: Build status ('success' or 'failed')
201
361
  """
@@ -218,14 +378,18 @@ class EnvironmentManager:
218
378
  elif mode == "clone":
219
379
  clone_env_script_path = pkg_resources.resource_filename('dataflow', 'scripts/clone_environment.sh')
220
380
  command = ["bash", clone_env_script_path, source_path, os.path.join(self.env_build_path, env_name), os.path.join(self.env_base_path, f"{env_name}_v{env_version}.squashfs"), yaml_file_path]
221
-
381
+
222
382
  elif mode == "local_clone":
223
383
  conda_cmd = f"conda create --name {env_name} --clone {source_path} --yes"
224
384
  command = ["su", "-", os.environ.get("NB_USER"), "-c", conda_cmd]
225
385
  self.logger.info(f"Cloning environment locally with command: {' '.join(command)}")
226
386
 
387
+ elif mode == "from_upper":
388
+ build_from_upper_script_path = pkg_resources.resource_filename('dataflow', 'scripts/build_from_upper.sh')
389
+ command = ["bash", build_from_upper_script_path, base_squashfs_path, tar_path, conda_env_path, scratch_dir, base_prefix, target_prefix]
390
+
227
391
  else:
228
- raise ValueError("Invalid mode. Use 'create' or 'clone'.")
392
+ raise ValueError("Invalid mode. Use 'create', 'clone', 'local_clone', or 'from_upper'.")
229
393
 
230
394
  process = await asyncio.create_subprocess_exec(
231
395
  *command,
@@ -439,8 +603,11 @@ class EnvironmentManager:
439
603
  Args:
440
604
  env_short_name (str): Short name of the environment
441
605
  version (str): Version of the environment
442
- pip_libraries (list): List of pip libraries
443
- conda_libraries (list): List of conda libraries
606
+ pip_libraries (list or None): List of pip libraries, or None to leave the
607
+ column untouched (e.g. a failed build never actually introspected
608
+ the new version's packages -- don't clobber whatever was there)
609
+ conda_libraries (list or None): List of conda libraries, or None to leave
610
+ the column untouched
444
611
  status (str): Build status ('success' or 'failed')
445
612
  db (Session): Database session
446
613
  """
@@ -452,13 +619,19 @@ class EnvironmentManager:
452
619
  current_env = db.query(Environment).filter(Environment.short_name == env_short_name).first()
453
620
  if not current_env:
454
621
  raise ValueError(f"Environment with short name '{env_short_name}' does not exist.")
455
-
622
+
456
623
  env_status = "Draft" if status == "success" else "Failed"
457
624
 
625
+ update_values = {"version": version, "status": env_status}
626
+ if pip_libraries is not None:
627
+ update_values["pip_libraries"] = pip_libraries
628
+ if conda_libraries is not None:
629
+ update_values["conda_libraries"] = conda_libraries
630
+
458
631
  db.query(Environment).filter(
459
632
  Environment.short_name == env_short_name,
460
633
  Environment.org_id == self.org_id if self.org_id is not None else Environment.org_id.is_(None)
461
- ).update({"version": version, "pip_libraries": pip_libraries, "conda_libraries": conda_libraries, "status": env_status})
634
+ ).update(update_values)
462
635
  db.commit()
463
636
 
464
637
  except Exception as e:
@@ -466,63 +639,46 @@ class EnvironmentManager:
466
639
  db.rollback()
467
640
  raise
468
641
 
469
- def update_library_versions(self, yaml_path: str):
642
+ def update_library_versions(self, env_path: str):
470
643
  """
471
- Updates libraries without version specifications by getting their actual installed versions from a conda YAML file.
472
-
473
- Args:
474
- yaml_path (str): Path to the conda environment YAML file.
475
-
644
+ Reads pip and conda packages from a built environment directory.
645
+
646
+ Uses pip freeze via the env's own Python binary so packages installed
647
+ with --target (e.g. dataflow-airflow via airflow_custom.pth) are included.
648
+ Runs conda list for conda-channel packages.
649
+
476
650
  Returns:
477
- tuple: Updated lists of (pip_libraries, conda_libraries) with version specifications.
651
+ tuple: (pip_libraries, conda_libraries) as lists of "name==version" / "name=version" strings.
478
652
  """
653
+ pip_libraries = []
479
654
  try:
480
- # Define default conda packages to ignore
481
- default_conda_packages = {
482
- "_libgcc_mutex", "_openmp_mutex", "bzip2", "ca-certificates",
483
- "ld_impl_linux-64", "libexpat", "libffi", "libgcc", "libgcc-ng",
484
- "libgomp", "liblzma", "libnsl", "libsqlite", "libuuid", "libxcrypt",
485
- "libzlib", "ncurses", "openssl", "readline", "setuptools", "tk",
486
- "tzdata", "wheel", "libstdcxx-ng", "python"
487
- }
488
-
489
- # Read the YAML file
490
- with open(yaml_path, 'r') as f:
491
- yaml_content = yaml.safe_load(f)
492
-
493
- # Extract conda and pip dependencies
494
- dependencies = yaml_content.get('dependencies', [])
495
-
496
- # Process conda libraries
497
- conda_libraries = []
498
- pip_libraries = []
499
-
500
- for dep in dependencies:
501
- if isinstance(dep, str):
502
- if dep.startswith("python="):
503
- continue
504
-
505
- parts = dep.split('=')
506
- package_name = parts[0].strip()
507
-
508
- if package_name.lower() not in default_conda_packages:
509
- if len(parts) >= 2:
510
- package_with_version = f"{package_name}={parts[1]}"
511
- conda_libraries.append(package_with_version)
512
- else:
513
- # No version specified, keep as is
514
- conda_libraries.append(dep)
515
-
516
- elif isinstance(dep, dict) and 'pip' in dep:
517
- # This is the pip section
518
- for pip_pkg in dep['pip']:
519
- pip_libraries.append(pip_pkg)
520
-
521
- return pip_libraries, conda_libraries
522
-
655
+ result = subprocess.run(
656
+ [f"{env_path}/bin/python", "-m", "pip", "freeze", "--disable-pip-version-check"],
657
+ capture_output=True, text=True, timeout=120
658
+ )
659
+ if result.returncode == 0:
660
+ for line in result.stdout.strip().splitlines():
661
+ line = line.strip()
662
+ if line and not line.startswith("#") and not line.startswith("-"):
663
+ pip_libraries.append(line)
664
+ else:
665
+ self.logger.error(f"pip freeze failed for {env_path}: {result.stderr.strip()}")
523
666
  except Exception as e:
524
- self.logger.critical(f"Error reading YAML file and extracting libraries: {str(e)}", exc_info=True, extra={"status_code": 500})
525
- return [], []
667
+ self.logger.error(f"Failed to get pip packages for {env_path}: {e}")
668
+
669
+ conda_libraries = []
670
+ try:
671
+ result = subprocess.run(
672
+ ["conda", "list", "--prefix", env_path, "--json"],
673
+ capture_output=True, text=True, check=True
674
+ )
675
+ for pkg in json.loads(result.stdout):
676
+ if pkg.get("channel") not in ("pypi", "<develop>") and pkg["name"] != "python":
677
+ conda_libraries.append(f"{pkg['name']}={pkg['version']}")
678
+ except Exception as e:
679
+ self.logger.error(f"Failed to get conda packages for {env_path}: {e}")
680
+
681
+ return pip_libraries, conda_libraries
526
682
 
527
683
  def create_conda_yaml(self, yaml_path, env_name, python_version, conda_packages, pip_packages):
528
684
  """
@@ -36,3 +36,4 @@ from .active_server_view import ActiveServerView
36
36
  from .inference_service import InferenceService, InferenceStatus
37
37
  from .onboarding_project import OnboardingProject
38
38
  from .ai_provider import AIProvider, AIModel, AIUserProviderKey
39
+ from .dbml_diagram import Diagram
@@ -0,0 +1,58 @@
1
+ from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, func, Enum, Index, text
2
+ from sqlalchemy.dialects.postgresql import JSONB, UUID
3
+ from dataflow.db import Base
4
+ import uuid
5
+
6
+
7
+ class Diagram(Base):
8
+
9
+ """TABLE 'DBML_DIAGRAM'
10
+
11
+ Stores a user-authored database ER diagram. The DBML source is the single source of
12
+ truth for the schema itself (tables, columns, relationships, header colours); the
13
+ layout column only holds presentation state that DBML has no syntax for, i.e. where
14
+ each table sits on the canvas and the last viewport.
15
+
16
+ Attributes:
17
+ id (int): Primary key for the diagram entry.
18
+ uid (UUID): Public identifier used in URLs, so the internal id is never exposed.
19
+ name (str): Display name of the diagram, unique per user within an organization.
20
+ description (str): Optional description of the diagram.
21
+ org_id (int): Foreign key referencing the ORGANIZATION table.
22
+ user_id (int): Foreign key referencing the USER table (the owner).
23
+ dbml_source (str): The DBML document defining the schema.
24
+ layout (dict): Canvas presentation state. Shape:
25
+ {"tables": {"public.users": {"x": 40, "y": 120}}, "viewport": {"x": 0, "y": 0, "zoom": 1}}
26
+ visibility (str): 'PRIVATE' or 'ORG'. Reserved for org sharing; currently always PRIVATE.
27
+ created_at (datetime): Timestamp of when the diagram was created.
28
+ updated_at (datetime): Timestamp of the last update to the diagram.
29
+ deleted_at (datetime): Soft delete timestamp; NULL while the diagram is active.
30
+
31
+ Constraints:
32
+ Index: Partial unique index ensuring a user cannot have two active diagrams
33
+ with the same name in the same organization.
34
+ """
35
+
36
+ __tablename__ = 'DBML_DIAGRAM'
37
+
38
+ id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
39
+ uid = Column(UUID(as_uuid=True), default=uuid.uuid4, nullable=False, unique=True, server_default=text("gen_random_uuid()"))
40
+ name = Column(String, nullable=False)
41
+ description = Column(Text, nullable=True)
42
+ org_id = Column(Integer, ForeignKey("ORGANIZATION.id", ondelete="CASCADE"), index=True, nullable=False)
43
+ user_id = Column(Integer, ForeignKey("USER.user_id", ondelete="CASCADE"), index=True, nullable=False)
44
+ dbml_source = Column(Text, nullable=False, server_default='')
45
+ layout = Column(JSONB, nullable=True)
46
+ visibility = Column(Enum("PRIVATE", "ORG", name="diagram_visibility"), default="PRIVATE", server_default="PRIVATE", nullable=False)
47
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
48
+ updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
49
+ deleted_at = Column(DateTime(timezone=True), nullable=True)
50
+
51
+ __table_args__ = (
52
+ Index(
53
+ 'uq_active_diagram_name',
54
+ 'name', 'user_id', 'org_id',
55
+ unique=True,
56
+ postgresql_where=deleted_at.is_(None)
57
+ ),
58
+ )
@@ -1,5 +1,5 @@
1
1
  from sqlalchemy import (
2
- Column, Integer, String, Boolean, Text,
2
+ Column, Integer, BigInteger, String, Boolean, Text,
3
3
  ForeignKey, DateTime, UniqueConstraint, CheckConstraint, Index
4
4
  )
5
5
  from sqlalchemy.orm import relationship, Session
@@ -181,6 +181,20 @@ class LocalEnvironment(Base):
181
181
  cloned_from (str): Source from which the environment was cloned.
182
182
  updated_at (datetime): Timestamp of when the local environment was last updated.
183
183
  need_refresh (bool): Indicates if the local environment needs a refresh.
184
+ base_env_id (int): ID of the base (squashfs) ENVIRONMENT this workset's delta sits on.
185
+ base_version (str): Version of the base environment the delta is keyed to, mirrors the
186
+ on-disk `_worksets/{short_name}_v{version}/` directory.
187
+ pip_diff (str): Precomputed package diff (added/removed/modified) captured at the last
188
+ checkpoint, serialized as JSON. Computed while the env is live so it never needs to
189
+ be re-derived from the (otherwise meaningless without its base) delta tar.
190
+ workset_path (str): Path to the persisted delta directory, relative to the user's jovyan
191
+ home, e.g. `.dataflow/envs/_worksets/{short_name}_v{version}`.
192
+ delta_size_bytes (int): Size in bytes of the last checkpointed delta.tar, for UI display
193
+ and retention/cleanup decisions.
194
+ is_workset (bool): True for auto-persisted workset shadow rows (one per user/base
195
+ env/version, tracking the overlay upperdir delta), as opposed to genuine
196
+ user-created local/conda environments which also live in this table. Queries that
197
+ list a user's real local environments must filter is_workset = false.
184
198
  """
185
199
 
186
200
  __tablename__ = "LOCAL_ENVIRONMENT"
@@ -197,6 +211,13 @@ class LocalEnvironment(Base):
197
211
  cloned_from = Column(String, nullable=True)
198
212
  updated_at = Column(DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
199
213
  need_refresh = Column(Boolean, default=False, server_default='false')
214
+ # Auto-persisted private-environment workset tracking (overlay upperdir delta).
215
+ base_env_id = Column(Integer, nullable=True)
216
+ base_version = Column(String, nullable=True)
217
+ pip_diff = Column(Text, nullable=True)
218
+ workset_path = Column(String, nullable=True)
219
+ delta_size_bytes = Column(BigInteger, nullable=True)
220
+ is_workset = Column(Boolean, default=False, server_default='false', nullable=False)
200
221
 
201
222
  class EnvType(str, Enum):
202
223
 
@@ -36,7 +36,13 @@ class ProjectDetails(Base):
36
36
  exposed_port (int): Port exposed by the custom image.
37
37
  private_repo (bool): Whether the image repository is private.
38
38
  registry_secret_id (int): Foreign key referencing the VARIABLE table for registry credentials.
39
-
39
+ source_type (str): Where the project's code comes from: GIT (cloned from git_url) or STUDIO
40
+ (synced from the creator's Studio folder into the runtime's projects volume).
41
+ studio_source_path (str): For STUDIO projects, the source folder relative to the creator's
42
+ /home/jovyan.
43
+ last_synced_at (datetime): For STUDIO projects, when files were last synced from Studio.
44
+ synced_by (str): For STUDIO projects, the user who ran the last sync.
45
+
40
46
  Relationships:
41
47
  app_type: Relationship to the AppType model.
42
48
  """
@@ -87,6 +93,15 @@ class ProjectDetails(Base):
87
93
  resource_config = Column(JSONB, nullable=True)
88
94
  cost_incurred = Column(JSONB, nullable=True)
89
95
  runtime_cost_state = Column(JSONB, nullable=True)
96
+ source_type = Column(
97
+ Enum("GIT", "STUDIO", name="project_source_type"),
98
+ nullable=False,
99
+ default="GIT",
100
+ server_default="GIT",
101
+ )
102
+ studio_source_path = Column(String, nullable=True)
103
+ last_synced_at = Column(DateTime(timezone=True), nullable=True)
104
+ synced_by = Column(String, nullable=True)
90
105
 
91
106
  __table_args__ = (
92
107
  Index(
@@ -0,0 +1,103 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # Reconstructs the overlayfs a user's activated environment ran with at
6
+ # runtime (read-only base squashfs as lowerdir + a captured upperdir tar of
7
+ # their pip/conda modifications), then repackages the merged result as a new
8
+ # squashfs. Requires CAP_SYS_ADMIN (loop + overlay mounts) and CAP_MKNOD
9
+ # (recreating whiteout char-devices on tar extract) -- run this in a
10
+ # privileged container.
11
+
12
+ base_squashfs_path=$1
13
+ tar_path=$2
14
+ squashfs_file_path=$3
15
+ scratch_dir=$4
16
+ base_prefix=$5
17
+ target_prefix=$6
18
+
19
+ if [ -z "$base_squashfs_path" ] || [ -z "$tar_path" ] || [ -z "$squashfs_file_path" ] \
20
+ || [ -z "$scratch_dir" ] || [ -z "$base_prefix" ] || [ -z "$target_prefix" ]; then
21
+ echo "Error: Missing required parameters"
22
+ exit 1
23
+ fi
24
+
25
+ if [ ! -f "$base_squashfs_path" ]; then
26
+ echo "Error: base squashfs file does not exist: $base_squashfs_path"
27
+ exit 1
28
+ fi
29
+
30
+ if [ ! -f "$tar_path" ]; then
31
+ echo "Error: upper tar file does not exist: $tar_path"
32
+ exit 1
33
+ fi
34
+
35
+ squashfs_file_name="$(basename "${squashfs_file_path}")"
36
+ env_name_with_version="${squashfs_file_name%.squashfs}"
37
+
38
+ lower_dir="${scratch_dir}/lower"
39
+ upper_dir="${scratch_dir}/upper"
40
+ work_dir="${scratch_dir}/work"
41
+ merged_dir="${base_prefix}"
42
+
43
+ # On failure, unwind whatever mounts were set up so we don't leak them. On
44
+ # success we deliberately leave lower/merged mounted, and leave a cloned
45
+ # target prefix in place -- the caller reads them (pip/conda introspection)
46
+ # before tearing everything down.
47
+ cleanup_on_failure() {
48
+ mountpoint -q "$merged_dir" 2>/dev/null && umount "$merged_dir" 2>/dev/null
49
+ mountpoint -q "$lower_dir" 2>/dev/null && umount "$lower_dir" 2>/dev/null
50
+ if [ "$target_prefix" != "$base_prefix" ]; then
51
+ rm -rf "$target_prefix"
52
+ fi
53
+ exit 1
54
+ }
55
+ trap cleanup_on_failure ERR
56
+
57
+ rm -rf "$scratch_dir"
58
+ mkdir -p "$lower_dir" "$upper_dir" "$work_dir"
59
+
60
+ # Defensive: a leftover mount here would silently become the clone source.
61
+ mountpoint -q "$merged_dir" 2>/dev/null && umount "$merged_dir"
62
+ rm -rf "$merged_dir"
63
+ mkdir -p "$merged_dir"
64
+
65
+ # lowerdir: loop-mount the base squashfs read-only -- identical to the
66
+ # lowerdir the user's pod ran against at runtime.
67
+ mount -t squashfs -o loop,ro "$base_squashfs_path" "$lower_dir"
68
+
69
+ # upperdir: extract the captured delta. --xattrs is mandatory -- an
70
+ # opaque-directory whiteout (a directory the user deleted and recreated) is
71
+ # recorded as the trusted.overlay.opaque xattr, not a regular tar entry, and
72
+ # silently vanishes on reconstruction without it. Character-device whiteout
73
+ # markers for deleted files require CAP_MKNOD to recreate on extract.
74
+ tar --xattrs --xattrs-include='*' --acls -xf "$tar_path" -C "$upper_dir"
75
+
76
+ # merged: reconstruct the overlay exactly as it was on the user's pod.
77
+ mount -t overlay overlay -o "lowerdir=${lower_dir},upperdir=${upper_dir},workdir=${work_dir}" "$merged_dir"
78
+
79
+ if [ "$base_prefix" = "$target_prefix" ]; then
80
+ # Same prefix: the merged view is already correct for where it will be
81
+ # mounted. Squash it as-is.
82
+ squashfs_source="$merged_dir"
83
+ else
84
+ echo "${base_prefix} to ${target_prefix}; cloning environment"
85
+
86
+ export PIP_NO_CACHE_DIR=1
87
+ export CONDA_PKGS_DIRS="${scratch_dir}/pkgs/${env_name_with_version}/"
88
+ mkdir -p "$CONDA_PKGS_DIRS"
89
+ conda config --set always_copy true
90
+
91
+ mkdir -p "$(dirname "$target_prefix")"
92
+ conda create --clone "$merged_dir" --prefix "$target_prefix" --yes
93
+
94
+ squashfs_source="$target_prefix"
95
+ fi
96
+
97
+ # Build the new squashfs from the merged (base + user modifications) view, or
98
+ # from the prefix-corrected clone of it.
99
+ mksquashfs "$squashfs_source" "${scratch_dir}/${squashfs_file_name}" -comp zstd -Xcompression-level 4 -force-uid 1000 -force-gid 1000
100
+ mkdir -p "$(dirname "$squashfs_file_path")"
101
+ cp -f "${scratch_dir}/${squashfs_file_name}" "$squashfs_file_path"
102
+
103
+ echo "Environment Creation Successful"
@@ -23,7 +23,7 @@ conda create --clone ${source_env_path} --prefix ${env_build_path} --yes
23
23
  conda env export --prefix "${env_build_path}" > "$yaml_file_path"
24
24
 
25
25
  # 2. Create squashfs file from the cloned environment
26
- mksquashfs "${env_build_path}" "${squashfs_file_name}" -comp zstd -Xcompression-level 4
26
+ mksquashfs "${env_build_path}" "${squashfs_file_name}" -comp zstd -Xcompression-level 4 -force-uid 1000 -force-gid 1000
27
27
  cp -f "${squashfs_file_name}" "${squashfs_file_path}"
28
28
  # 3. Cleanup the temporary environment directory
29
29
  rm -rf "${env_build_path}"