evo-files 0.1.0__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.
evo/files/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ # Copyright © 2025 Bentley Systems, Incorporated
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software
7
+ # distributed under the License is distributed on an "AS IS" BASIS,
8
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ # See the License for the specific language governing permissions and
10
+ # limitations under the License.
11
+
12
+ """FileAPI SDK
13
+ =====================
14
+ """
15
+
16
+ from .client import FileAPIClient
17
+ from .data import FileMetadata, FileVersion
18
+ from .io import FileAPIDownload, FileAPIUpload
19
+
20
+ __all__ = [
21
+ "FileAPIClient",
22
+ "FileAPIDownload",
23
+ "FileAPIUpload",
24
+ "FileMetadata",
25
+ "FileVersion",
26
+ ]
@@ -0,0 +1,20 @@
1
+ # Copyright © 2025 Bentley Systems, Incorporated
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software
7
+ # distributed under the License is distributed on an "AS IS" BASIS,
8
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ # See the License for the specific language governing permissions and
10
+ # limitations under the License.
11
+
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+
15
+ class CustomBaseModel(BaseModel):
16
+ """Custom base model for providing a global configuration to generated models."""
17
+
18
+ model_config = ConfigDict(
19
+ extra="allow",
20
+ )
evo/files/client.py ADDED
@@ -0,0 +1,327 @@
1
+ # Copyright © 2025 Bentley Systems, Incorporated
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software
7
+ # distributed under the License is distributed on an "AS IS" BASIS,
8
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ # See the License for the specific language governing permissions and
10
+ # limitations under the License.
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import PurePosixPath
15
+ from uuid import UUID
16
+
17
+ from evo import logging
18
+ from evo.common import APIConnector, BaseAPIClient, Environment, HealthCheckType, Page, ServiceHealth, ServiceUser
19
+ from evo.common.utils import get_service_health
20
+
21
+ from .data import FileMetadata, FileVersion
22
+ from .endpoints import FileV2Api
23
+ from .endpoints.models import DownloadFileResponse, FileVersionResponse, ListFile, UserInfo
24
+ from .io import FileAPIDownload, FileAPIUpload
25
+
26
+ logger = logging.getLogger("file.client")
27
+
28
+ __all__ = ["FileAPIClient"]
29
+
30
+
31
+ def _user_from_model(model: UserInfo | None) -> ServiceUser | None:
32
+ """Create a ServiceUser instance from a generated UserInfo model.
33
+
34
+ :param model: The model to create the ServiceUser instance from, or None.
35
+
36
+ :return: A ServiceUser instance, or None if the model is None.
37
+ """
38
+ return None if model is None else ServiceUser(id=model.id, name=model.name, email=model.email)
39
+
40
+
41
+ def _versions_from_listed_versions(models: list[FileVersionResponse]) -> list[FileVersion]:
42
+ """Create a list of FileVersion instances from a list of generated FileVersionResponse models.
43
+
44
+ :param models: The models to create the FileVersion instances from.
45
+
46
+ :return: A sorted list of FileVersion instances.
47
+ """
48
+ versions = (
49
+ FileVersion(
50
+ version_id=model.version_id,
51
+ created_at=model.created_at,
52
+ created_by=_user_from_model(model.created_by),
53
+ )
54
+ for model in models
55
+ )
56
+ return sorted(versions, key=lambda v: v.created_at, reverse=True)
57
+
58
+
59
+ class FileAPIClient(BaseAPIClient):
60
+ def __init__(self, environment: Environment, connector: APIConnector) -> None:
61
+ """
62
+ :param environment: The environment object
63
+ :param connector: The connector object.
64
+ """
65
+ super().__init__(environment, connector)
66
+ self._api = FileV2Api(connector=connector)
67
+
68
+ async def get_service_health(self, check_type: HealthCheckType = HealthCheckType.FULL) -> ServiceHealth:
69
+ """Get the health of the file service.
70
+
71
+ :param check_type: The type of health check to perform.
72
+
73
+ :return: A ServiceHealth object.
74
+
75
+ :raises EvoAPIException: If the API returns an unexpected status code.
76
+ :raises ClientValueError: If the response is not a valid service health check response.
77
+ """
78
+ return await get_service_health(self._connector, "file", check_type=check_type)
79
+
80
+ def _metadata_from_listed_file(self, model: ListFile) -> FileMetadata:
81
+ """Create a FileMetadata instance from a generated ListFile model.
82
+
83
+ :param model: The model to create the FileMetadata instance from.
84
+
85
+ :return: A FileMetadata instance.
86
+ """
87
+ return FileMetadata(
88
+ environment=self._environment,
89
+ id=model.file_id,
90
+ name=model.name,
91
+ created_at=model.created_at,
92
+ created_by=_user_from_model(model.created_by),
93
+ modified_at=model.modified_at,
94
+ modified_by=_user_from_model(model.modified_by),
95
+ parent=model.path,
96
+ version_id=model.version_id,
97
+ size=model.size,
98
+ )
99
+
100
+ def _metadata_from_endpoint_model(self, model: DownloadFileResponse) -> FileMetadata:
101
+ """Create a FileMetadata instance from a generated DownloadFileResponse model.
102
+
103
+ :param model: The model to create the FileMetadata instance from.
104
+
105
+ :return: A FileMetadata instance.
106
+ """
107
+ file_path = PurePosixPath(model.path)
108
+ return FileMetadata(
109
+ environment=self._environment,
110
+ id=model.file_id,
111
+ name=model.name,
112
+ created_at=model.created_at,
113
+ created_by=_user_from_model(model.created_by),
114
+ modified_at=model.modified_at,
115
+ modified_by=_user_from_model(model.modified_by),
116
+ parent=str(file_path.parent),
117
+ version_id=model.version_id,
118
+ size=model.size,
119
+ )
120
+
121
+ async def list_files(
122
+ self,
123
+ offset: int = 0,
124
+ limit: int = 5000,
125
+ name: str | None = None,
126
+ ) -> Page[FileMetadata]:
127
+ """List up to `limit` files in the workspace, starting at `offset`.
128
+
129
+ The files will be the latest version of the file.
130
+ If there are no files starting at `offset`, the page will be empty.
131
+
132
+ :param offset: The number of files to skip before listing.
133
+ :param limit: Max number of files to list.
134
+ :param name: Filter files by name.
135
+
136
+ :return: A page of all files from the query.
137
+ """
138
+ assert limit > 0, "Limit must be a positive integer"
139
+ assert offset >= 0, "Offset must be a non-negative integer"
140
+ response = await self._api.list_files(
141
+ organisation_id=str(self._environment.org_id),
142
+ workspace_id=str(self._environment.workspace_id),
143
+ limit=limit,
144
+ offset=offset,
145
+ file_name=name,
146
+ )
147
+ return Page(
148
+ offset=offset,
149
+ limit=limit,
150
+ total=response.total,
151
+ items=[self._metadata_from_listed_file(file) for file in response.files],
152
+ )
153
+
154
+ async def list_all_files(self, limit_per_request: int = 5000, name: str | None = None) -> list[FileMetadata]:
155
+ """List all files in the workspace.
156
+
157
+ This method makes multiple calls to the `list_files` endpoint until all files have been listed.
158
+
159
+ :param limit_per_request: The maximum number of files to list in one request.
160
+ :param name: Filter files by name.
161
+
162
+ :return: A list of all files in the workspace.
163
+ """
164
+ items = []
165
+ offset = 0
166
+ while True:
167
+ page = await self.list_files(offset=offset, limit=limit_per_request, name=name)
168
+ items += page.items()
169
+ if page.is_last:
170
+ break
171
+ offset = page.next_offset
172
+ return items
173
+
174
+ async def get_file_by_path(self, path: str, version_id: str | None = None) -> FileMetadata:
175
+ """Get a file by its path.
176
+
177
+ :param path: The path to the file.
178
+ :param version_id: ID of the desired file version. By default, the response will return the latest version.
179
+ :return: A FileMetadata representation of the file on the service.
180
+ """
181
+ file_response = await self._api.get_file_by_path(
182
+ organisation_id=str(self._environment.org_id),
183
+ workspace_id=str(self._environment.workspace_id),
184
+ file_path=path,
185
+ version_id=version_id,
186
+ )
187
+ return self._metadata_from_endpoint_model(file_response)
188
+
189
+ async def get_file_by_id(self, file_id: UUID, version_id: str | None = None) -> FileMetadata:
190
+ """Get a file by its ID.
191
+
192
+ :param file_id: UUID of a file
193
+ :param version_id: ID of the desired file version. By default, the response will return the latest version.
194
+ :return: A FileMetadata representation of the file on the service
195
+ """
196
+ file_response = await self._api.get_file_by_id(
197
+ organisation_id=str(self._environment.org_id),
198
+ workspace_id=str(self._environment.workspace_id),
199
+ file_id=str(file_id),
200
+ version_id=version_id,
201
+ )
202
+ return self._metadata_from_endpoint_model(file_response)
203
+
204
+ async def list_versions_by_path(self, path: str) -> list[FileVersion]:
205
+ """List the versions of a file by path.
206
+
207
+ :param path: The path to the file.
208
+ :return: A sorted list of file versions. The latest version is the first element of the list.
209
+ """
210
+ file_response = await self._api.get_file_by_path(
211
+ organisation_id=str(self._environment.org_id),
212
+ workspace_id=str(self._environment.workspace_id),
213
+ file_path=path,
214
+ include_versions=True,
215
+ )
216
+ return _versions_from_listed_versions(file_response.versions)
217
+
218
+ async def list_versions_by_id(self, file_id: UUID) -> list[FileVersion]:
219
+ """List the versions of a file by ID
220
+
221
+ :param file_id: UUID of the file.
222
+ :return: A sorted list of file versions. The latest version is the first element of the list.
223
+ """
224
+ file_response = await self._api.get_file_by_id(
225
+ organisation_id=str(self._environment.org_id),
226
+ workspace_id=str(self._environment.workspace_id),
227
+ file_id=str(file_id),
228
+ include_versions=True,
229
+ )
230
+ return _versions_from_listed_versions(file_response.versions)
231
+
232
+ async def prepare_download_by_path(self, path: str, version_id: str | None = None) -> FileAPIDownload:
233
+ """Prepares a file for download by path.
234
+
235
+ :param path: Path to the file.
236
+ :param version_id: Versions of the file.
237
+
238
+ :return: A FileAPIDownload object.
239
+ """
240
+ response = await self._api.get_file_by_path(
241
+ organisation_id=str(self._environment.org_id),
242
+ workspace_id=str(self._environment.workspace_id),
243
+ file_path=path,
244
+ version_id=version_id,
245
+ )
246
+ metadata = self._metadata_from_endpoint_model(response)
247
+ return FileAPIDownload(connector=self._connector, metadata=metadata, initial_url=response.download)
248
+
249
+ async def prepare_download_by_id(self, file_id: UUID, version_id: str | None = None) -> FileAPIDownload:
250
+ """Prepares a file for download by ID.
251
+
252
+ :param file_id: UUID of the file.
253
+ :param version_id: Version of the file.
254
+
255
+ :return: A FileAPIDownload object.
256
+ """
257
+ response = await self._api.get_file_by_id(
258
+ organisation_id=str(self._environment.org_id),
259
+ workspace_id=str(self._environment.workspace_id),
260
+ file_id=str(file_id),
261
+ version_id=version_id,
262
+ )
263
+ metadata = self._metadata_from_endpoint_model(response)
264
+ return FileAPIDownload(connector=self._connector, metadata=metadata, initial_url=response.download)
265
+
266
+ async def prepare_upload_by_path(self, path: str) -> FileAPIUpload:
267
+ """Prepares a file for upload by path. If the file already exists, a new version will be created.
268
+
269
+ :param path: Path the file is being uploaded to.
270
+
271
+ :return: A FileAPIUpload object.
272
+ """
273
+ response = await self._api.upsert_file_by_path(
274
+ organisation_id=str(self._environment.org_id),
275
+ workspace_id=str(self._environment.workspace_id),
276
+ file_path=path,
277
+ )
278
+ return FileAPIUpload(
279
+ connector=self._connector,
280
+ environment=self._environment,
281
+ file_id=response.file_id,
282
+ version_id=response.version_id,
283
+ initial_url=response.upload,
284
+ )
285
+
286
+ async def prepare_upload_by_id(self, file_id: UUID) -> FileAPIUpload:
287
+ """Prepares a file for upload by ID. The file_id must be the ID of an existing file, for which a new version
288
+ will be created.
289
+
290
+ :param file_id: UUID of the file.
291
+
292
+ :return: A FileAPIUpload object.
293
+ """
294
+ response = await self._api.update_file_by_id(
295
+ organisation_id=str(self._environment.org_id),
296
+ workspace_id=str(self._environment.workspace_id),
297
+ file_id=str(file_id),
298
+ )
299
+ return FileAPIUpload(
300
+ connector=self._connector,
301
+ environment=self._environment,
302
+ file_id=file_id,
303
+ version_id=response.version_id,
304
+ initial_url=response.upload,
305
+ )
306
+
307
+ async def delete_file_by_path(self, path: str) -> None:
308
+ """Deletes a file by path.
309
+
310
+ :param path: Path of the file to delete.
311
+ """
312
+ await self._api.delete_file_by_path(
313
+ organisation_id=str(self._environment.org_id),
314
+ workspace_id=str(self._environment.workspace_id),
315
+ file_path=path,
316
+ )
317
+
318
+ async def delete_file_by_id(self, file_id: UUID) -> None:
319
+ """Deletes a file by ID.
320
+
321
+ :param file_id: UUID of the file to delete.
322
+ """
323
+ await self._api.delete_file_by_id(
324
+ organisation_id=str(self._environment.org_id),
325
+ workspace_id=str(self._environment.workspace_id),
326
+ file_id=str(file_id),
327
+ )
evo/files/data.py ADDED
@@ -0,0 +1,74 @@
1
+ # Copyright © 2025 Bentley Systems, Incorporated
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software
7
+ # distributed under the License is distributed on an "AS IS" BASIS,
8
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ # See the License for the specific language governing permissions and
10
+ # limitations under the License.
11
+
12
+ from __future__ import annotations
13
+
14
+ __all__ = [
15
+ "FileMetadata",
16
+ "FileVersion",
17
+ ]
18
+
19
+ from dataclasses import dataclass
20
+ from datetime import datetime
21
+
22
+ from evo.common import ResourceMetadata, ServiceUser
23
+
24
+
25
+ @dataclass(frozen=True, kw_only=True)
26
+ class FileMetadata(ResourceMetadata):
27
+ """Metadata about a file in the File API."""
28
+
29
+ parent: str
30
+ """The parent path of the file."""
31
+
32
+ version_id: str
33
+ """An arbitrary identifier for the file version."""
34
+
35
+ size: int
36
+ """The size of the file in bytes."""
37
+
38
+ modified_at: datetime
39
+ """The resource's last modified timestamp."""
40
+
41
+ modified_by: ServiceUser | None = None
42
+ """The user who last modified the resource."""
43
+
44
+ @property
45
+ def path(self) -> str:
46
+ """The full path of the file, formed by joining the parent and name, separated by a slash ('/')."""
47
+ return f"{self.parent.removesuffix('/')}/{self.name.removeprefix('/')}"
48
+
49
+ @property
50
+ def url(self) -> str:
51
+ """The URL of the file in the File API."""
52
+ return (
53
+ "{hub_url}/file/v2/orgs/{org_id}/workspaces/{workspace_id}/files/{file_id}?version_id={version_id}".format(
54
+ hub_url=self.environment.hub_url.rstrip("/"),
55
+ org_id=self.environment.org_id,
56
+ workspace_id=self.environment.workspace_id,
57
+ file_id=self.id,
58
+ version_id=self.version_id,
59
+ )
60
+ )
61
+
62
+
63
+ @dataclass(frozen=True, kw_only=True)
64
+ class FileVersion:
65
+ """Represents a version of a file."""
66
+
67
+ version_id: str
68
+ """An arbitrary identifier for the file version."""
69
+
70
+ created_at: datetime
71
+ """The date and time when the file version was created."""
72
+
73
+ created_by: ServiceUser | None
74
+ """The user who uploaded the file version."""
@@ -0,0 +1,35 @@
1
+ # Copyright © 2025 Bentley Systems, Incorporated
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software
7
+ # distributed under the License is distributed on an "AS IS" BASIS,
8
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ # See the License for the specific language governing permissions and
10
+ # limitations under the License.
11
+ """
12
+ File API
13
+ =============
14
+
15
+ The File API provides the ability to manage files of any type or size, associated with
16
+ your Evo workspace. Enable your product with Evo connected workflows by integrating with the Seequent Evo
17
+ File API. Most file formats and sizes are accepted.
18
+
19
+ Files can be referenced by their UUID, or by a user-defined file path. Files are versioned, so updating or
20
+ replacing them will create a new version of the file. The latest version of the file is always returned
21
+ unless a specific version is requested.
22
+
23
+ For more information on using the File API, see [Overview](https://developer.seequent.com/docs/guides/file/), or the API references here.
24
+
25
+
26
+ This code is generated from the OpenAPI specification for File API.
27
+ API version: 2.8.0
28
+ """
29
+
30
+ # Import endpoint apis.
31
+ from .api import FileV2Api
32
+
33
+ __all__ = [
34
+ "FileV2Api",
35
+ ]
@@ -0,0 +1,12 @@
1
+ # Copyright © 2025 Bentley Systems, Incorporated
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software
7
+ # distributed under the License is distributed on an "AS IS" BASIS,
8
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9
+ # See the License for the specific language governing permissions and
10
+ # limitations under the License.
11
+
12
+ from .file_v2_api import FileV2Api # noqa: F401