metafold 0.13.1.dev0__tar.gz → 0.13.2.dev0__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 (35) hide show
  1. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/PKG-INFO +1 -1
  2. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/simulation/compression_simulation.py +45 -24
  3. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/simulation/run_experiment.py +11 -0
  4. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold.egg-info/PKG-INFO +1 -1
  5. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/pyproject.toml +1 -1
  6. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_compression_simulation.py +71 -0
  7. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/LICENSE +0 -0
  8. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/README.md +0 -0
  9. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/__init__.py +0 -0
  10. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/api.py +0 -0
  11. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/assets.py +0 -0
  12. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/auth.py +0 -0
  13. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/client.py +0 -0
  14. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/exceptions.py +0 -0
  15. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/jobs.py +0 -0
  16. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/materials.py +0 -0
  17. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/projects.py +0 -0
  18. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/simulation/__init__.py +0 -0
  19. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/simulation/compression_experiment.py +0 -0
  20. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/utils.py +0 -0
  21. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold/workflows.py +0 -0
  22. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold.egg-info/SOURCES.txt +0 -0
  23. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold.egg-info/dependency_links.txt +0 -0
  24. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold.egg-info/requires.txt +0 -0
  25. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/metafold.egg-info/top_level.txt +0 -0
  26. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/setup.cfg +0 -0
  27. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_assets.py +0 -0
  28. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_compession_experiment.py +0 -0
  29. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_jobs.py +0 -0
  30. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_materials.py +0 -0
  31. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_projects.py +0 -0
  32. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_run_experiment.py +0 -0
  33. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_shear_simulation.py +0 -0
  34. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_utils.py +0 -0
  35. {metafold-0.13.1.dev0 → metafold-0.13.2.dev0}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.13.1.dev0
3
+ Version: 0.13.2.dev0
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -299,6 +299,39 @@ class ExperimentSupportParallelepiped(ExperimentParallelepiped, ExperimentSuppor
299
299
  material: Material = field(default_factory=lambda: DEFAULT_SUPPORT_MATERIAL)
300
300
 
301
301
 
302
+ def validate_simulation_parts(parts: list) -> None:
303
+ """Raise ValueError if the parts can't produce a compression simulation.
304
+
305
+ Callers that dispatch work asynchronously should call this up front so
306
+ invalid input fails before any project or job is created.
307
+ """
308
+ if not parts:
309
+ return
310
+
311
+ has_piston = False
312
+ has_support = False
313
+ piston_moves = False
314
+ for p in parts:
315
+ if isinstance(p, ExperimentPistonBase):
316
+ has_piston = True
317
+ piston_moves = piston_moves or _has_motion(p.velocity)
318
+ elif isinstance(p, ExperimentSupportBase):
319
+ has_support = True
320
+
321
+ if not has_piston and not has_support:
322
+ raise ValueError(
323
+ "No piston part: a compression simulation needs "
324
+ "a rigid part to apply load. Add a part of type piston_mesh, "
325
+ "piston_cylinder, or piston_box."
326
+ )
327
+ # Supports are stationary by design, so only pistons must move.
328
+ if has_piston and not piston_moves:
329
+ raise ValueError(
330
+ "Piston velocity is zero. Set a velocity "
331
+ "profile of [time, vx, vy, vz] rows with a non-zero component."
332
+ )
333
+
334
+
302
335
  @dataclass
303
336
  class ReferenceData:
304
337
  csv_path: Path = Path("")
@@ -491,28 +524,7 @@ class CompressionSimulation:
491
524
  self.part_infos.append(inner_part)
492
525
 
493
526
  # A rigid part drives the compression.
494
- if parts:
495
- has_piston = False
496
- has_support = False
497
- piston_moves = False
498
- for p in parts:
499
- if isinstance(p, ExperimentPistonBase):
500
- has_piston = True
501
- piston_moves = piston_moves or _has_motion(p.velocity)
502
- elif isinstance(p, ExperimentSupportBase):
503
- has_support = True
504
- if not has_piston and not has_support:
505
- raise ValueError(
506
- "No piston part: a compression simulation needs "
507
- "a rigid part to apply load. Add a part of type piston_mesh, "
508
- "piston_cylinder, or piston_box."
509
- )
510
- # Supports are stationary by design, so only pistons must move.
511
- if has_piston and not piston_moves:
512
- raise ValueError(
513
- "Piston velocity is zero. Set a velocity "
514
- "profile of [time, vx, vy, vz] rows with a non-zero component."
515
- )
527
+ validate_simulation_parts(parts)
516
528
 
517
529
  self.setup_ply_files(stl_folder_path)
518
530
  self.setup_results(output_path)
@@ -644,6 +656,7 @@ class CompressionSimulation:
644
656
  base_url: str = "https://api.metafold3d.com/",
645
657
  cancel_existing_workflows: bool = True,
646
658
  project_access: Access = Access.PRIVATE,
659
+ user: Optional[str] = None,
647
660
  ) -> str:
648
661
  """Find an existing project by name or create a new one.
649
662
 
@@ -651,6 +664,10 @@ class CompressionSimulation:
651
664
  client_id + client_secret (local/programmable use, reads from env via
652
665
  setup_client). base_url must match the audience the token was issued for.
653
666
 
667
+ When user is given, only the caller's own projects are reused; a
668
+ same-named project owned by someone else (visible under org access) is
669
+ ignored so it can't be overwritten.
670
+
654
671
  When cancel_existing_workflows is True and an existing project is
655
672
  reused, its running workflows are cancelled first.
656
673
 
@@ -663,10 +680,14 @@ class CompressionSimulation:
663
680
  auth_domain=auth_domain,
664
681
  base_url=base_url,
665
682
  )
666
- # Quote the project_name string for multi-word name searchess
683
+ # Quote the values so multi-word names stay a single search term.
684
+ q = f'name:"{project_name}"'
685
+ if user is not None:
686
+ q = f'{q} AND user:"{user}"'
667
687
  existing = [
668
- p for p in client.projects.list(q=f'name:"{project_name}"')
688
+ p for p in client.projects.list(q=q)
669
689
  if p.name == project_name # ensure full match
690
+ and (user is None or p.user == user)
670
691
  ]
671
692
  if existing:
672
693
  project_id = existing[0].id
@@ -160,6 +160,7 @@ from metafold.simulation.compression_simulation import (
160
160
  ExperimentPistonMesh,
161
161
  SimulationParameters,
162
162
  WorkflowStep,
163
+ validate_simulation_parts,
163
164
  )
164
165
 
165
166
  # Build preset lookup from all Material constants in the materials module.
@@ -243,6 +244,16 @@ def _build_parts(parts_config: list[dict]) -> list[ExperimentPart]:
243
244
  return parts
244
245
 
245
246
 
247
+ def validate_experiment_config(config: dict) -> None:
248
+ """Raise ValueError if a manifest can't produce a runnable experiment.
249
+ Mirrors the checks run_experiment performs so an API can reject bad
250
+ input before creating a project.
251
+ """
252
+ if not config.get("project_name", ""):
253
+ raise ValueError("project_name is required")
254
+ validate_simulation_parts(_build_parts(config.get("parts", [])))
255
+
256
+
246
257
  def _build_varying(varying_config: list[dict]) -> list[ExperimentVarying]:
247
258
  varying: list[ExperimentVarying] = []
248
259
  for entry in varying_config:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metafold
3
- Version: 0.13.1.dev0
3
+ Version: 0.13.2.dev0
4
4
  Summary: Metafold SDK for Python
5
5
  Author-email: Metafold 3D <info@metafold3d.com>
6
6
  License: Copyright 2024 Metafold 3D
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "metafold"
7
- version = "0.13.1.dev0"
7
+ version = "0.13.2.dev0"
8
8
  authors = [
9
9
  {name = "Metafold 3D", email = "info@metafold3d.com"},
10
10
  ]
@@ -1439,3 +1439,74 @@ class TestFindOrCreateProjectCancel:
1439
1439
 
1440
1440
  assert pid == "new-pid"
1441
1441
  client.workflows.cancel.assert_not_called()
1442
+
1443
+
1444
+ class TestFindOrCreateProjectUserScope:
1445
+ """With user set, only the caller's own project is reused; a same-named
1446
+ project owned by someone else (visible under org access) is ignored."""
1447
+
1448
+ @pytest.fixture
1449
+ def patched_client_class(self, monkeypatch):
1450
+ from metafold.simulation import compression_simulation
1451
+ fake_class = MagicMock()
1452
+ monkeypatch.setattr(compression_simulation, "MetafoldClient", fake_class)
1453
+ return fake_class
1454
+
1455
+ def _project(self, pid, name, user):
1456
+ p = MagicMock()
1457
+ p.id = pid
1458
+ p.name = name
1459
+ p.user = user
1460
+ return p
1461
+
1462
+ def test_ignores_same_name_owned_by_other_user(self, patched_client_class):
1463
+ client = patched_client_class.return_value
1464
+ # A defensive belt-and-suspenders case: even if the API returned another
1465
+ # user's project, the Python filter drops it and we create a new one.
1466
+ client.projects.list.return_value = [self._project("other-pid", "shared", "userB")]
1467
+ client.projects.create.return_value = self._project("new-pid", "shared", "userA")
1468
+
1469
+ pid = CompressionSimulation.find_or_create_project(
1470
+ "shared", access_token="tok", base_url="http://x/", user="userA",
1471
+ )
1472
+
1473
+ assert pid == "new-pid"
1474
+ client.projects.create.assert_called_once()
1475
+
1476
+ def test_reuses_callers_own_project(self, patched_client_class):
1477
+ client = patched_client_class.return_value
1478
+ client.projects.list.return_value = [self._project("mine", "shared", "userA")]
1479
+
1480
+ pid = CompressionSimulation.find_or_create_project(
1481
+ "shared", access_token="tok", base_url="http://x/", user="userA",
1482
+ cancel_existing_workflows=False,
1483
+ )
1484
+
1485
+ assert pid == "mine"
1486
+ client.projects.create.assert_not_called()
1487
+
1488
+ def test_scopes_query_to_user(self, patched_client_class):
1489
+ client = patched_client_class.return_value
1490
+ client.projects.list.return_value = []
1491
+ client.projects.create.return_value = self._project("new-pid", "shared", "userA")
1492
+
1493
+ CompressionSimulation.find_or_create_project(
1494
+ "shared", access_token="tok", base_url="http://x/", user="userA",
1495
+ )
1496
+
1497
+ q = client.projects.list.call_args.kwargs["q"]
1498
+ assert 'name:"shared"' in q
1499
+ assert 'user:"userA"' in q
1500
+
1501
+ def test_no_user_keeps_unscoped_behavior(self, patched_client_class):
1502
+ client = patched_client_class.return_value
1503
+ client.projects.list.return_value = [self._project("any-pid", "shared", "userB")]
1504
+
1505
+ pid = CompressionSimulation.find_or_create_project(
1506
+ "shared", access_token="tok", base_url="http://x/",
1507
+ cancel_existing_workflows=False,
1508
+ )
1509
+
1510
+ assert pid == "any-pid"
1511
+ q = client.projects.list.call_args.kwargs["q"]
1512
+ assert "user:" not in q
File without changes
File without changes
File without changes