Skip to content

Commit e777535

Browse files
committed
Merge branch 'main' into feature/add-design-workflow-datasource
2 parents b03680b + 2d5352c commit e777535

File tree

5 files changed

+131
-29
lines changed

5 files changed

+131
-29
lines changed

src/citrine/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.9.0"
1+
__version__ = "3.10.0"

src/citrine/resources/file_link.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from urllib.parse import urlparse
88
from urllib.request import url2pathname
99
from uuid import UUID
10+
from warnings import warn
1011

1112
from citrine._rest.collection import Collection
1213
from citrine._rest.resource import GEMDResource
@@ -676,7 +677,8 @@ def ingest(self,
676677
delete_dataset_contents: bool = False,
677678
delete_templates: bool = True,
678679
timeout: float = None,
679-
polling_delay: Optional[float] = None
680+
polling_delay: Optional[float] = None,
681+
project: Optional[Union["Project", UUID, str]] = None, # noqa: F821
680682
) -> "IngestionStatus": # noqa: F821
681683
"""
682684
[ALPHA] Ingest a set of CSVs and/or Excel Workbooks formatted per the gemd-ingest protocol.
@@ -702,6 +704,8 @@ def ingest(self,
702704
build_table: bool
703705
Whether to trigger a regeneration of the table config and building the table
704706
after ingestion. Default: False
707+
project: Optional[Project, UUID, or str]
708+
Which project to use for table build if build_table is True.
705709
delete_dataset_contents: bool
706710
Whether to delete old objects prior to creating new ones. Default: False
707711
delete_templates: bool
@@ -721,6 +725,18 @@ def ingest(self,
721725
722726
"""
723727
from citrine.resources.ingestion import IngestionCollection
728+
from citrine.resources.project import Project # noqa: F401
729+
730+
if build_table and project is None:
731+
if self.project_id is None:
732+
raise ValueError("Building a table requires a target project.")
733+
else:
734+
warn(
735+
"Building a table with an implicit project is deprecated "
736+
"and will be removed in v4. Please pass a project explicitly.",
737+
DeprecationWarning
738+
)
739+
project = self.project_id
724740

725741
def resolve_with_local(candidate: Union[FileLink, Path, str]) -> FileLink:
726742
"""Resolve Path, str or FileLink to an absolute reference."""
@@ -765,6 +781,7 @@ def resolve_with_local(candidate: Union[FileLink, Path, str]) -> FileLink:
765781
)
766782
return ingestion.build_objects(
767783
build_table=build_table,
784+
project=project,
768785
delete_dataset_contents=delete_dataset_contents,
769786
delete_templates=delete_templates,
770787
timeout=timeout,

src/citrine/resources/ingestion.py

+56-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from typing import Optional, Iterator, Iterable
1+
from deprecation import deprecated
2+
from typing import Optional, Union, Iterator, Iterable, Collection as TypingCollection
23
from uuid import UUID
4+
from warnings import warn
35

46
from gemd.enumeration.base_enumeration import BaseEnumeration
57

@@ -188,14 +190,26 @@ class Ingestion(Resource['Ingestion']):
188190
uid = properties.UUID('ingestion_id')
189191
"""UUID: Unique uuid4 identifier of this ingestion."""
190192
team_id = properties.Optional(properties.UUID, 'team_id', default=None)
191-
project_id = properties.Optional(properties.UUID, 'project_id', default=None)
193+
_project_id = properties.Optional(properties.UUID, 'project_id', default=None)
192194
dataset_id = properties.UUID('dataset_id')
193195
session = properties.Object(Session, 'session', serializable=False)
194196
raise_errors = properties.Optional(properties.Boolean(), 'raise_errors', default=True)
195197

198+
@property
199+
def project_id(self) -> Optional[UUID]:
200+
"""[DEPRECATED] The project ID associated with this ingest."""
201+
return self._project_id
202+
203+
@project_id.setter
204+
@deprecated(deprecated_in='3.9.0', removed_in='4.0.0',
205+
details="Use the project argument instead of setting the project_id attribute.")
206+
def project_id(self, value: Optional[UUID]):
207+
self._project_id = value
208+
196209
def build_objects(self,
197210
*,
198211
build_table: bool = False,
212+
project: Optional[Union["Project", UUID, str]] = None, # noqa: F821
199213
delete_dataset_contents: bool = False,
200214
delete_templates: bool = True,
201215
timeout: float = None,
@@ -211,6 +225,8 @@ def build_objects(self,
211225
----------
212226
build_table: bool
213227
Whether to build a table immediately after ingestion. Default : False
228+
project: Optional[Project, UUID, or str]
229+
Which project to use for table build if build_table is True.
214230
delete_dataset_contents: bool
215231
Whether to delete objects prior to generating new gemd objects. Default: False.
216232
delete_templates: bool
@@ -231,6 +247,7 @@ def build_objects(self,
231247
"""
232248
try:
233249
job = self.build_objects_async(build_table=build_table,
250+
project=project,
234251
delete_dataset_contents=delete_dataset_contents,
235252
delete_templates=delete_templates)
236253
except IngestionException as e:
@@ -249,6 +266,7 @@ def build_objects(self,
249266
def build_objects_async(self,
250267
*,
251268
build_table: bool = False,
269+
project: Optional[Union["Project", UUID, str]] = None, # noqa: F821
252270
delete_dataset_contents: bool = False,
253271
delete_templates: bool = True) -> JobSubmissionResponse:
254272
"""
@@ -258,6 +276,8 @@ def build_objects_async(self,
258276
----------
259277
build_table: bool
260278
Whether to build a table immediately after ingestion. Default : False
279+
project: Optional[Project, UUID, or str]
280+
Which project to use for table build if build_table is True.
261281
delete_dataset_contents: bool
262282
Whether to delete objects prior to generating new gemd objects. Default: False.
263283
delete_templates: bool
@@ -270,12 +290,35 @@ def build_objects_async(self,
270290
The object for the submitted job
271291
272292
"""
293+
from citrine.resources.project import Project
273294
collection = IngestionCollection(team_id=self.team_id,
274295
dataset_id=self.dataset_id,
275296
session=self.session)
276297
path = collection._get_path(uid=self.uid, action="gemd-objects-async")
298+
299+
# Project resolution logic
300+
if not build_table:
301+
project_id = None
302+
elif project is None:
303+
if self.project_id is None:
304+
raise ValueError("Building a table requires a target project.")
305+
else:
306+
warn(
307+
"Building a table with an implicit project is deprecated "
308+
"and will be removed in v4. Please pass a project explicitly.",
309+
DeprecationWarning
310+
)
311+
project_id = self.project_id
312+
elif isinstance(project, Project):
313+
project_id = project.uid
314+
elif isinstance(project, UUID):
315+
project_id = project
316+
else:
317+
project_id = UUID(project)
318+
277319
params = {
278320
"build_table": build_table,
321+
"project_id": project_id,
279322
"delete_dataset_contents": delete_dataset_contents,
280323
"delete_templates": delete_templates,
281324
}
@@ -358,20 +401,25 @@ def __init__(self, errors: Iterable[IngestionErrorTrace]):
358401
def build_objects(self,
359402
*,
360403
build_table: bool = False,
404+
project: Optional[Union["Project", UUID, str]] = None, # noqa: F821
361405
delete_dataset_contents: bool = False,
362-
delete_templates: bool = True) -> IngestionStatus:
406+
delete_templates: bool = True,
407+
timeout: float = None,
408+
polling_delay: Optional[float] = None
409+
) -> IngestionStatus:
363410
"""[ALPHA] Satisfy the required interface for a failed ingestion."""
364411
return self.status()
365412

366413
def build_objects_async(self,
367414
*,
368415
build_table: bool = False,
416+
project: Optional[Union["Project", UUID, str]] = None, # noqa: F821
369417
delete_dataset_contents: bool = False,
370418
delete_templates: bool = True) -> JobSubmissionResponse:
371419
"""[ALPHA] Satisfy the required interface for a failed ingestion."""
372420
raise JobFailureError(
373421
message=f"Errors: {[e.msg for e in self.errors]}",
374-
job_id=None,
422+
job_id=UUID('0' * 32), # Nil UUID
375423
failure_reasons=[e.msg for e in self.errors]
376424
)
377425

@@ -384,7 +432,7 @@ def poll_for_job_completion(self,
384432
"""[ALPHA] Satisfy the required interface for a failed ingestion."""
385433
raise JobFailureError(
386434
message=f"Errors: {[e.msg for e in self.errors]}",
387-
job_id=None,
435+
job_id=UUID('0' * 32), # Nil UUID
388436
failure_reasons=[e.msg for e in self.errors]
389437
)
390438

@@ -401,8 +449,8 @@ def status(self) -> IngestionStatus:
401449
if self.raise_errors:
402450
raise JobFailureError(
403451
message=f"Ingestion creation failed: {self.errors}",
404-
job_id=None,
405-
failure_reasons=self.errors
452+
job_id=UUID('0' * 32), # Nil UUID
453+
failure_reasons=[str(x) for x in self.errors]
406454
)
407455
else:
408456
return IngestionStatus.build({
@@ -461,7 +509,7 @@ def _path_template(self):
461509
return f'projects/{self.project_id}/ingestions'
462510

463511
def build_from_file_links(self,
464-
file_links: Iterable[FileLink],
512+
file_links: TypingCollection[FileLink],
465513
*,
466514
raise_errors: bool = True) -> Ingestion:
467515
"""

tests/resources/test_file_link.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Iterable
2+
from typing import Collection
33
from uuid import uuid4, UUID
44

55
import pytest
@@ -90,11 +90,11 @@ def test_deprecation_of_positional_arguments(session):
9090
check_project = {'project': {'team': {'id': team_id}}}
9191
session.set_response(check_project)
9292
with pytest.deprecated_call():
93-
fcol = FileCollection(uuid4(), uuid4(), session)
93+
_ = FileCollection(uuid4(), uuid4(), session)
9494
with pytest.raises(TypeError):
95-
fcol = FileCollection(project_id=uuid4(), dataset_id=uuid4(), session=None)
95+
_ = FileCollection(project_id=uuid4(), dataset_id=uuid4(), session=None)
9696
with pytest.raises(TypeError):
97-
fcol = FileCollection(project_id=uuid4(), dataset_id=None, session=session)
97+
_ = FileCollection(project_id=uuid4(), dataset_id=None, session=session)
9898

9999
def test_delete(collection: FileCollection, session):
100100
"""Test that deletion calls the expected endpoint and checks the url structure."""
@@ -569,6 +569,15 @@ def test_ingest(collection: FileCollection, session):
569569
with pytest.raises(TypeError):
570570
collection.ingest([Path(good_file1.url)])
571571

572+
with pytest.raises(ValueError):
573+
collection.ingest([good_file1], build_table=True)
574+
575+
session.set_responses(ingest_create_resp, job_id_resp, job_status_resp, ingest_status_resp)
576+
coll_with_project_id = FileCollection(team_id=uuid4(), dataset_id=uuid4(), session=session)
577+
coll_with_project_id.project_id = uuid4()
578+
with pytest.deprecated_call():
579+
coll_with_project_id.ingest([good_file1], build_table=True)
580+
572581

573582
def test_ingest_with_upload(collection, monkeypatch, tmp_path, session):
574583
"""Test more advanced workflows, patching to avoid unnecessary complexity."""
@@ -591,7 +600,7 @@ def _mock_upload(self, *, file_path, dest_name=None):
591600
return FileLink(url='relative/path', filename=file_path.name)
592601

593602
def _mock_build_from_file_links(self: IngestionCollection,
594-
file_links: Iterable[FileLink],
603+
file_links: Collection[FileLink],
595604
*,
596605
raise_errors: bool = True
597606
):

tests/resources/test_ingestion.py

+42-14
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from citrine.resources.file_link import FileLink
99
from citrine.resources.ingestion import Ingestion, IngestionCollection, IngestionStatus, IngestionStatusType, \
1010
IngestionException, IngestionErrorTrace, IngestionErrorType, IngestionErrorFamily, IngestionErrorLevel
11-
from citrine.jobs.job import JobSubmissionResponse, JobStatusResponse, JobFailureError, _poll_for_job_completion
11+
from citrine.jobs.job import JobSubmissionResponse, JobStatusResponse, JobFailureError
12+
from citrine.resources.project import Project
1213

1314
from tests.utils.factories import DatasetFactory
1415
from tests.utils.session import FakeCall, FakeSession, FakeRequestResponseApiError
@@ -31,12 +32,12 @@ def dataset(session: Session):
3132

3233
@pytest.fixture
3334
def deprecated_dataset(session: Session):
34-
dataset = DatasetFactory(name='Test Dataset')
35-
dataset.uid = uuid4()
36-
dataset.session = session
37-
dataset.project_id = uuid4()
35+
deprecated_dataset = DatasetFactory(name='Test Dataset')
36+
deprecated_dataset.uid = uuid4()
37+
deprecated_dataset.session = session
38+
deprecated_dataset.project_id = uuid4()
3839

39-
return dataset
40+
return deprecated_dataset
4041

4142

4243
@pytest.fixture
@@ -105,11 +106,11 @@ def test_deprecation_of_positional_arguments(session):
105106
check_project = {'project': {'team': {'id': team_id}}}
106107
session.set_response(check_project)
107108
with pytest.deprecated_call():
108-
ingestion_collection = IngestionCollection(uuid4(), uuid4(), session)
109+
IngestionCollection(uuid4(), uuid4(), session)
109110
with pytest.raises(TypeError):
110-
ingestion_collection = IngestionCollection(project_id=uuid4(), dataset_id=uuid4(), session=None)
111+
IngestionCollection(project_id=uuid4(), dataset_id=uuid4(), session=None)
111112
with pytest.raises(TypeError):
112-
ingestion_collection = IngestionCollection(project_id=uuid4(), dataset_id=None, session=session)
113+
IngestionCollection(project_id=uuid4(), dataset_id=None, session=session)
113114

114115

115116
def test_poll_for_job_completion_signature(ingest, operation, status, monkeypatch):
@@ -262,6 +263,38 @@ def _mock_poll_for_job_completion(**_):
262263
assert any('Sad' in e.msg for e in result.errors)
263264

264265

266+
def test_ingestion_with_table_build(session: FakeSession,
267+
ingest: Ingestion,
268+
dataset: Dataset,
269+
deprecated_dataset: Dataset,
270+
file_link: FileLink):
271+
# build_objects_async will always approve, if we get that far
272+
session.set_responses(
273+
{"job_id": str(uuid4())}
274+
)
275+
276+
with pytest.raises(ValueError):
277+
ingest.build_objects_async(build_table=True)
278+
279+
with pytest.deprecated_call():
280+
ingest.project_id = uuid4()
281+
with pytest.deprecated_call():
282+
ingest.build_objects_async(build_table=True)
283+
with pytest.deprecated_call():
284+
ingest.project_id = None
285+
286+
project_uuid = uuid4()
287+
project = Project("Testing", session=session, team_id=dataset.team_id)
288+
project.uid = project_uuid
289+
ingest.build_objects_async(build_table=True, project=project)
290+
assert session.last_call.params["project_id"] == project_uuid
291+
292+
ingest.build_objects_async(build_table=True, project=project_uuid)
293+
assert session.last_call.params["project_id"] == project_uuid
294+
295+
ingest.build_objects_async(build_table=True, project=str(project_uuid))
296+
assert session.last_call.params["project_id"] == project_uuid
297+
265298
def test_ingestion_flow(session: FakeSession,
266299
ingest: Ingestion,
267300
collection: IngestionCollection,
@@ -324,8 +357,3 @@ def _raise_exception():
324357
)
325358
with pytest.raises(IngestionException, match="Missing ingredient"):
326359
ingest.build_objects()
327-
328-
329-
def test_invalid_poll_for_job_completion(session):
330-
with pytest.raises(TypeError):
331-
_poll_for_job_completion(session=session, job=uuid4(), project_id=None, team_id=None)

0 commit comments

Comments
 (0)