Skip to content

Commit 410bbec

Browse files
authored
Merge pull request #4 from tektronix/artifacts
File artifacts
2 parents cb278bf + d333b1a commit 410bbec

File tree

17 files changed

+414
-56
lines changed

17 files changed

+414
-56
lines changed

docs/reference/models.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Models
77
:maxdepth: 2
88
:caption: Models
99

10+
models/artifact
1011
models/file
1112
models/folder
1213
models/member

docs/reference/models/artifact.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. _artifact:
2+
3+
Artifact
4+
========
5+
6+
.. autoclass:: tekdrive.models.Artifact
7+
:inherited-members:
8+
:exclude-members: parse

tekdrive/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class SharingType(Enum):
88

99

1010
class ObjectType(Enum):
11+
ARTIFACT = "ARTIFACT"
1112
FILE = "FILE"
1213
FOLDER = "FOLDER"
1314

@@ -19,6 +20,7 @@ class FolderType(Enum):
1920

2021

2122
class ErrorCode(Enum):
23+
ARTIFACT_NOT_FOUND = "ARTIFACT_NOT_FOUND"
2224
FILE_GONE = "FILE_GONE"
2325
FILE_NOT_FOUND = "FILE_NOT_FOUND"
2426
FOLDER_GONE = "FOLDER_GONE"

tekdrive/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ def request_id(self):
3939
return self.headers.get("X-Request-Id")
4040

4141

42+
class ArtifactNotFoundAPIException(TekDriveAPIException):
43+
"""Indicate artifact is not found."""
44+
45+
4246
class FileNotFoundAPIException(TekDriveAPIException):
4347
"""Indicate file is not found."""
4448

tekdrive/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .drive.file import File # noqa
22
from .drive.folder import Folder # noqa
3+
from .drive.artifact import Artifact, ArtifactsList # noqa
34
from .drive.member import Member, MembersList # noqa
45
from .drive.plan import Plan # noqa
56
from .drive.trash import Trash # noqa

tekdrive/models/drive/artifact.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Provides the Artifact class."""
2+
from typing import TYPE_CHECKING, Optional, Dict, Any, Union
3+
4+
from .base import DriveBase, Downloadable
5+
from ..base import BaseList
6+
from ...routing import Route, ENDPOINTS
7+
8+
if TYPE_CHECKING:
9+
from .. import TekDrive
10+
11+
12+
class Artifact(DriveBase, Downloadable):
13+
"""
14+
Represents a file artifact.
15+
16+
Attributes:
17+
bytes (str): Artifact size in bytes.
18+
created_at (datetime): When the artifact was created.
19+
context_type (str): Context to identify the type of artifact such as ``"SETTING"`` or ``"CHANNEL"``.
20+
file_id (str): ID of the file that the artifact is associated with.
21+
file_type (str): Artifact file type such as ``"TSS"`` or ``"SET"``.
22+
id (str): Unique artifact ID.
23+
name (str): Artifact name.
24+
parent_artifact_id: ID of the parent artifact.
25+
updated_at (datetime, optional): When the artifact was last updated.
26+
"""
27+
28+
STR_FIELD = "id"
29+
30+
@classmethod
31+
def from_data(cls, tekdrive, data):
32+
return cls(tekdrive, data)
33+
34+
def __init__(
35+
self,
36+
tekdrive: "TekDrive",
37+
_data: Optional[Dict[str, Any]] = None,
38+
):
39+
super().__init__(tekdrive, _data=_data)
40+
41+
def __setattr__(
42+
self,
43+
attribute: str,
44+
value: Union[str, int, Dict[str, Any]],
45+
):
46+
super().__setattr__(attribute, value)
47+
48+
def _fetch_download_url(self):
49+
route = Route("GET", ENDPOINTS["file_artifact_download"], file_id=self.file_id, artifact_id=self.id)
50+
download_details = self._tekdrive.request(route)
51+
return download_details["download_url"]
52+
53+
54+
class ArtifactsList(BaseList):
55+
"""List of artifacts"""
56+
57+
_parent = None
58+
59+
LIST_ATTRIBUTE = "artifacts"

tekdrive/models/drive/base.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Provide the DriveBase class."""
2-
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
2+
import os
3+
import requests
4+
from abc import ABC, abstractmethod
5+
from typing import TYPE_CHECKING, Any, Dict, IO, Optional, Union
36

47
from ..base import TekDriveBase
8+
from ...exceptions import ClientException, TekDriveStorageException
59

610
if TYPE_CHECKING:
711
from ... import TekDrive
@@ -70,3 +74,61 @@ def _reset_attributes(self, *attributes):
7074
if attribute in self.__dict__:
7175
del self.__dict__[attribute]
7276
self._fetched = False
77+
78+
79+
class Downloadable(ABC):
80+
"""Abstract base class for download functionality."""
81+
82+
@abstractmethod
83+
def _fetch_download_url(self):
84+
pass
85+
86+
def _download_from_storage(self):
87+
download_url = self._fetch_download_url()
88+
try:
89+
r = requests.get(
90+
download_url,
91+
)
92+
r.raise_for_status()
93+
return r.content
94+
except requests.exceptions.HTTPError as exception:
95+
raise TekDriveStorageException("Download failed") from exception
96+
97+
def download(self, path_or_writable: Union[str, IO] = None) -> None:
98+
"""
99+
Download contents.
100+
101+
Args:
102+
path_or_writable: Path to a local file or a writable stream
103+
where contents will be written.
104+
105+
Raises:
106+
ClientException: If invalid file path is given.
107+
108+
Examples:
109+
Download to local file using path::
110+
111+
here = os.path.dirname(__file__)
112+
contents_path = os.path.join(here, "test_file_overwrite.txt")
113+
file.download(contents_path)
114+
115+
Download using writable stream::
116+
117+
with open("./download.csv", "wb") as f:
118+
file.download(f)
119+
"""
120+
if path_or_writable is None:
121+
# return content directly
122+
return self._download_from_storage()
123+
124+
if isinstance(path_or_writable, str):
125+
file_path = path_or_writable
126+
127+
if not os.path.exists(file_path):
128+
raise ClientException(f"File '{file_path}' does not exist.")
129+
130+
with open(file_path, "wb") as file:
131+
file.write(self._download_from_storage())
132+
else:
133+
writable = path_or_writable
134+
writable.write(self._download_from_storage())

tekdrive/models/drive/file.py

Lines changed: 50 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from ...routing import Route, ENDPOINTS
88
from ...exceptions import ClientException, TekDriveStorageException
99
from ...utils.casing import to_snake_case, to_camel_case
10-
from .base import DriveBase
10+
from .base import DriveBase, Downloadable
11+
from .artifact import Artifact, ArtifactsList
1112
from .member import Member, MembersList
1213
from .user import PartialUser
1314
from ...enums import ObjectType
@@ -17,7 +18,7 @@
1718
from .. import TekDrive
1819

1920

20-
class File(DriveBase):
21+
class File(DriveBase, Downloadable):
2122
"""
2223
A class representing a TekDrive file.
2324
@@ -120,17 +121,6 @@ def _upload_to_storage(self, file: IO):
120121
except requests.exceptions.HTTPError as exception:
121122
raise TekDriveStorageException("Upload failed") from exception
122123

123-
def _download_from_storage(self):
124-
download_url = self._fetch_download_url()
125-
try:
126-
r = requests.get(
127-
download_url,
128-
)
129-
r.raise_for_status()
130-
return r.content
131-
except requests.exceptions.HTTPError as exception:
132-
raise TekDriveStorageException("Upload failed") from exception
133-
134124
@staticmethod
135125
def _create(
136126
_tekdrive,
@@ -154,6 +144,52 @@ def _create(
154144

155145
return new_file
156146

147+
def artifacts(self, flat: bool = False) -> ArtifactsList:
148+
"""
149+
Get a list of file artifacts.
150+
151+
Args:
152+
flat: Return artifacts in flat list with no child nesting?
153+
154+
Examples:
155+
Iterate over all file artifacts::
156+
157+
for artifact in file.artifacts():
158+
print(artifact.name)
159+
160+
Returns:
161+
List [ :ref:`artifact` ]
162+
"""
163+
params = to_camel_case(dict(flat=flat))
164+
165+
route = Route("GET", ENDPOINTS["file_artifacts"], file_id=self.id)
166+
artifacts = self._tekdrive.request(route, params=params)
167+
artifacts._parent = self
168+
return artifacts
169+
170+
def artifact(self, artifact_id: str, depth: int = 1) -> Artifact:
171+
"""
172+
Get a file artifact by ID.
173+
174+
Args:
175+
artifact_id: Unique ID for the artifact
176+
depth: How many nested levels of child artifacts to return.
177+
178+
Examples:
179+
Load artifact with children up to 3 levels deep::
180+
181+
artifact_id = "017820c4-03ba-4e9d-be2f-e0ba346ddd9b"
182+
artifact = file.artifact(artifact_id, depth=3)
183+
184+
Returns:
185+
:ref:`artifact`
186+
"""
187+
params = to_camel_case(dict(depth=depth))
188+
189+
route = Route("GET", ENDPOINTS["file_artifact"], file_id=self.id, artifact_id=artifact_id)
190+
artifact = self._tekdrive.request(route, params=params)
191+
return artifact
192+
157193
def members(self) -> MembersList:
158194
"""
159195
Get a list of file members.
@@ -225,7 +261,7 @@ def upload(self, path_or_readable: Union[str, IO]) -> None:
225261
226262
Upload using readable stream::
227263
228-
with open("./test_file.txt"), "rb") as f:
264+
with open("./test_file.txt", "rb") as f:
229265
new_file.upload(f)
230266
"""
231267
# TODO: multipart upload support
@@ -241,45 +277,6 @@ def upload(self, path_or_readable: Union[str, IO]) -> None:
241277
readable = path_or_readable
242278
self._upload_to_storage(readable)
243279

244-
def download(self, path_or_writable: Union[str, IO] = None) -> None:
245-
"""
246-
Download file contents.
247-
248-
Args:
249-
path_or_writable: Path to a local file or a writable stream
250-
where file contents will be written.
251-
252-
Raises:
253-
ClientException: If invalid file path is given.
254-
255-
Examples:
256-
Download to local file using path::
257-
258-
here = os.path.dirname(__file__)
259-
contents_path = os.path.join(here, "test_file_overwrite.txt")
260-
file.download(contents_path)
261-
262-
Download using writable stream::
263-
264-
with open("./download.csv"), "wb") as f:
265-
file.download(f)
266-
"""
267-
if path_or_writable is None:
268-
# return content directly
269-
return self._download_from_storage()
270-
271-
if isinstance(path_or_writable, str):
272-
file_path = path_or_writable
273-
274-
if not os.path.exists(file_path):
275-
raise ClientException(f"File '{file_path}' does not exist.")
276-
277-
with open(file_path, "wb") as file:
278-
file.write(self._download_from_storage())
279-
else:
280-
writable = path_or_writable
281-
writable.write(self._download_from_storage())
282-
283280
def move(self, parent_folder_id: str) -> None:
284281
"""
285282
Move file to a different folder.

tekdrive/models/parser.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def __init__(self, tekdrive: "TekDrive", models: Optional[Dict[str, Any]] = None
3939
self._tekdrive = tekdrive
4040
self.models = {} if models is None else models
4141

42+
def _is_artifact(self, data: dict) -> bool:
43+
return data.get("type") == ObjectType.ARTIFACT.value
44+
45+
def _is_artifacts_list(self, data: dict) -> bool:
46+
return "artifacts" in data
47+
4248
def _is_file(self, data: dict) -> bool:
4349
return data.get("type") == ObjectType.FILE.value
4450

@@ -73,6 +79,10 @@ def _parse_dict(self, data: dict):
7379
model = self.models["File"]
7480
elif self._is_folder(data):
7581
model = self.models["Folder"]
82+
elif self._is_artifacts_list(data):
83+
model = self.models["ArtifactsList"]
84+
elif self._is_artifact(data):
85+
model = self.models["Artifact"]
7686
elif self._is_members_list(data):
7787
model = self.models["MembersList"]
7888
elif self._is_member(data):

tekdrive/routing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def __init__(self, method, path_template, **params):
2222

2323

2424
ENDPOINTS = {
25+
"file_artifacts": "/file/{file_id}/artifacts",
26+
"file_artifact": "/file/{file_id}/artifacts/{artifact_id}",
27+
"file_artifact_download": "/file/{file_id}/artifacts/{artifact_id}/contents",
2528
"file_create": "/file",
2629
"file_details": "/file/{file_id}",
2730
"file_delete": "/file/{file_id}",

tekdrive/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Settings/constants"""
22

3-
__version__ = "1.0.0"
3+
__version__ = "1.1.0"
44

55
RATELIMIT_SECONDS = 1
66
TIMEOUT = 15

tekdrive/tekdrive.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def __exit__(self, *_args):
6464

6565
def _create_model_map(self):
6666
model_map = {
67+
"Artifact": models.Artifact,
68+
"ArtifactsList": models.ArtifactsList,
6769
"File": models.File,
6870
"Folder": models.Folder,
6971
"Member": models.Member,

0 commit comments

Comments
 (0)