singlestoredb 1.8.0__py3-none-any.whl → 1.9.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.

Potentially problematic release.


This version of singlestoredb might be problematic. Click here for more details.

@@ -0,0 +1,1038 @@
1
+ #!/usr/bin/env python
2
+ """SingleStore Cloud Files Management."""
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import io
7
+ import os
8
+ import re
9
+ from abc import ABC
10
+ from abc import abstractmethod
11
+ from typing import Any
12
+ from typing import BinaryIO
13
+ from typing import Dict
14
+ from typing import List
15
+ from typing import Optional
16
+ from typing import TextIO
17
+ from typing import Union
18
+
19
+ from .. import config
20
+ from ..exceptions import ManagementError
21
+ from .manager import Manager
22
+ from .utils import PathLike
23
+ from .utils import to_datetime
24
+ from .utils import vars_to_str
25
+
26
+
27
+ PERSONAL_SPACE = 'personal'
28
+ SHARED_SPACE = 'shared'
29
+
30
+
31
+ class FilesObject(object):
32
+ """
33
+ File / folder object.
34
+
35
+ It can belong to either a workspace stage or personal/shared space.
36
+
37
+ This object is not instantiated directly. It is used in the results
38
+ of various operations in ``WorkspaceGroup.stage``, ``FilesManager.personal_space``
39
+ and ``FilesManager.shared_space`` methods.
40
+
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ path: str,
47
+ size: int,
48
+ type: str,
49
+ format: str,
50
+ mimetype: str,
51
+ created: Optional[datetime.datetime],
52
+ last_modified: Optional[datetime.datetime],
53
+ writable: bool,
54
+ content: Optional[List[str]] = None,
55
+ ):
56
+ #: Name of file / folder
57
+ self.name = name
58
+
59
+ if type == 'directory':
60
+ path = re.sub(r'/*$', r'', str(path)) + '/'
61
+
62
+ #: Path of file / folder
63
+ self.path = path
64
+
65
+ #: Size of the object (in bytes)
66
+ self.size = size
67
+
68
+ #: Data type: file or directory
69
+ self.type = type
70
+
71
+ #: Data format
72
+ self.format = format
73
+
74
+ #: Mime type
75
+ self.mimetype = mimetype
76
+
77
+ #: Datetime the object was created
78
+ self.created_at = created
79
+
80
+ #: Datetime the object was modified last
81
+ self.last_modified_at = last_modified
82
+
83
+ #: Is the object writable?
84
+ self.writable = writable
85
+
86
+ #: Contents of a directory
87
+ self.content: List[str] = content or []
88
+
89
+ self._location: Optional[FileLocation] = None
90
+
91
+ @classmethod
92
+ def from_dict(
93
+ cls,
94
+ obj: Dict[str, Any],
95
+ location: FileLocation,
96
+ ) -> FilesObject:
97
+ """
98
+ Construct a FilesObject from a dictionary of values.
99
+
100
+ Parameters
101
+ ----------
102
+ obj : dict
103
+ Dictionary of values
104
+ location : FileLocation
105
+ FileLocation object to use as the parent
106
+
107
+ Returns
108
+ -------
109
+ :class:`FilesObject`
110
+
111
+ """
112
+ out = cls(
113
+ name=obj['name'],
114
+ path=obj['path'],
115
+ size=obj['size'],
116
+ type=obj['type'],
117
+ format=obj['format'],
118
+ mimetype=obj['mimetype'],
119
+ created=to_datetime(obj.get('created')),
120
+ last_modified=to_datetime(obj.get('last_modified')),
121
+ writable=bool(obj['writable']),
122
+ )
123
+ out._location = location
124
+ return out
125
+
126
+ def __str__(self) -> str:
127
+ """Return string representation."""
128
+ return vars_to_str(self)
129
+
130
+ def __repr__(self) -> str:
131
+ """Return string representation."""
132
+ return str(self)
133
+
134
+ def open(
135
+ self,
136
+ mode: str = 'r',
137
+ encoding: Optional[str] = None,
138
+ ) -> Union[io.StringIO, io.BytesIO]:
139
+ """
140
+ Open a file path for reading or writing.
141
+
142
+ Parameters
143
+ ----------
144
+ mode : str, optional
145
+ The read / write mode. The following modes are supported:
146
+ * 'r' open for reading (default)
147
+ * 'w' open for writing, truncating the file first
148
+ * 'x' create a new file and open it for writing
149
+ The data type can be specified by adding one of the following:
150
+ * 'b' binary mode
151
+ * 't' text mode (default)
152
+ encoding : str, optional
153
+ The string encoding to use for text
154
+
155
+ Returns
156
+ -------
157
+ FilesObjectBytesReader - 'rb' or 'b' mode
158
+ FilesObjectBytesWriter - 'wb' or 'xb' mode
159
+ FilesObjectTextReader - 'r' or 'rt' mode
160
+ FilesObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode
161
+
162
+ """
163
+ if self._location is None:
164
+ raise ManagementError(
165
+ msg='No FileLocation object is associated with this object.',
166
+ )
167
+
168
+ if self.is_dir():
169
+ raise IsADirectoryError(
170
+ f'directories can not be read or written: {self.path}',
171
+ )
172
+
173
+ return self._location.open(self.path, mode=mode, encoding=encoding)
174
+
175
+ def download(
176
+ self,
177
+ local_path: Optional[PathLike] = None,
178
+ *,
179
+ overwrite: bool = False,
180
+ encoding: Optional[str] = None,
181
+ ) -> Optional[Union[bytes, str]]:
182
+ """
183
+ Download the content of a file path.
184
+
185
+ Parameters
186
+ ----------
187
+ local_path : Path or str
188
+ Path to local file target location
189
+ overwrite : bool, optional
190
+ Should an existing file be overwritten if it exists?
191
+ encoding : str, optional
192
+ Encoding used to convert the resulting data
193
+
194
+ Returns
195
+ -------
196
+ bytes or str or None
197
+
198
+ """
199
+ if self._location is None:
200
+ raise ManagementError(
201
+ msg='No FileLocation object is associated with this object.',
202
+ )
203
+
204
+ return self._location.download_file(
205
+ self.path, local_path=local_path,
206
+ overwrite=overwrite, encoding=encoding,
207
+ )
208
+
209
+ download_file = download
210
+
211
+ def remove(self) -> None:
212
+ """Delete the file."""
213
+ if self._location is None:
214
+ raise ManagementError(
215
+ msg='No FileLocation object is associated with this object.',
216
+ )
217
+
218
+ if self.type == 'directory':
219
+ raise IsADirectoryError(
220
+ f'path is a directory; use rmdir or removedirs {self.path}',
221
+ )
222
+
223
+ self._location.remove(self.path)
224
+
225
+ def rmdir(self) -> None:
226
+ """Delete the empty directory."""
227
+ if self._location is None:
228
+ raise ManagementError(
229
+ msg='No FileLocation object is associated with this object.',
230
+ )
231
+
232
+ if self.type != 'directory':
233
+ raise NotADirectoryError(
234
+ f'path is not a directory: {self.path}',
235
+ )
236
+
237
+ self._location.rmdir(self.path)
238
+
239
+ def removedirs(self) -> None:
240
+ """Delete the directory recursively."""
241
+ if self._location is None:
242
+ raise ManagementError(
243
+ msg='No FileLocation object is associated with this object.',
244
+ )
245
+
246
+ if self.type != 'directory':
247
+ raise NotADirectoryError(
248
+ f'path is not a directory: {self.path}',
249
+ )
250
+
251
+ self._location.removedirs(self.path)
252
+
253
+ def rename(self, new_path: PathLike, *, overwrite: bool = False) -> None:
254
+ """
255
+ Move the file to a new location.
256
+
257
+ Parameters
258
+ ----------
259
+ new_path : Path or str
260
+ The new location of the file
261
+ overwrite : bool, optional
262
+ Should path be overwritten if it already exists?
263
+
264
+ """
265
+ if self._location is None:
266
+ raise ManagementError(
267
+ msg='No FileLocation object is associated with this object.',
268
+ )
269
+ out = self._location.rename(self.path, new_path, overwrite=overwrite)
270
+ self.name = out.name
271
+ self.path = out.path
272
+ return None
273
+
274
+ def exists(self) -> bool:
275
+ """Does the file / folder exist?"""
276
+ if self._location is None:
277
+ raise ManagementError(
278
+ msg='No FileLocation object is associated with this object.',
279
+ )
280
+ return self._location.exists(self.path)
281
+
282
+ def is_dir(self) -> bool:
283
+ """Is the object a directory?"""
284
+ return self.type == 'directory'
285
+
286
+ def is_file(self) -> bool:
287
+ """Is the object a file?"""
288
+ return self.type != 'directory'
289
+
290
+ def abspath(self) -> str:
291
+ """Return the full path of the object."""
292
+ return str(self.path)
293
+
294
+ def basename(self) -> str:
295
+ """Return the basename of the object."""
296
+ return self.name
297
+
298
+ def dirname(self) -> str:
299
+ """Return the directory name of the object."""
300
+ return re.sub(r'/*$', r'', os.path.dirname(re.sub(r'/*$', r'', self.path))) + '/'
301
+
302
+ def getmtime(self) -> float:
303
+ """Return the last modified datetime as a UNIX timestamp."""
304
+ if self.last_modified_at is None:
305
+ return 0.0
306
+ return self.last_modified_at.timestamp()
307
+
308
+ def getctime(self) -> float:
309
+ """Return the creation datetime as a UNIX timestamp."""
310
+ if self.created_at is None:
311
+ return 0.0
312
+ return self.created_at.timestamp()
313
+
314
+
315
+ class FilesObjectTextWriter(io.StringIO):
316
+ """StringIO wrapper for writing to FileLocation."""
317
+
318
+ def __init__(self, buffer: Optional[str], location: FileLocation, path: PathLike):
319
+ self._location = location
320
+ self._path = path
321
+ super().__init__(buffer)
322
+
323
+ def close(self) -> None:
324
+ """Write the content to the path."""
325
+ self._location._upload(self.getvalue(), self._path)
326
+ super().close()
327
+
328
+
329
+ class FilesObjectTextReader(io.StringIO):
330
+ """StringIO wrapper for reading from FileLocation."""
331
+
332
+
333
+ class FilesObjectBytesWriter(io.BytesIO):
334
+ """BytesIO wrapper for writing to FileLocation."""
335
+
336
+ def __init__(self, buffer: bytes, location: FileLocation, path: PathLike):
337
+ self._location = location
338
+ self._path = path
339
+ super().__init__(buffer)
340
+
341
+ def close(self) -> None:
342
+ """Write the content to the file path."""
343
+ self._location._upload(self.getvalue(), self._path)
344
+ super().close()
345
+
346
+
347
+ class FilesObjectBytesReader(io.BytesIO):
348
+ """BytesIO wrapper for reading from FileLocation."""
349
+
350
+
351
+ class FileLocation(ABC):
352
+ @abstractmethod
353
+ def open(
354
+ self,
355
+ path: PathLike,
356
+ mode: str = 'r',
357
+ encoding: Optional[str] = None,
358
+ ) -> Union[io.StringIO, io.BytesIO]:
359
+ pass
360
+
361
+ @abstractmethod
362
+ def upload_file(
363
+ self,
364
+ local_path: Union[PathLike, TextIO, BinaryIO],
365
+ path: PathLike,
366
+ *,
367
+ overwrite: bool = False,
368
+ ) -> FilesObject:
369
+ pass
370
+
371
+ @abstractmethod
372
+ def upload_folder(
373
+ self,
374
+ local_path: PathLike,
375
+ path: PathLike,
376
+ *,
377
+ overwrite: bool = False,
378
+ recursive: bool = True,
379
+ include_root: bool = False,
380
+ ignore: Optional[Union[PathLike, List[PathLike]]] = None,
381
+ ) -> FilesObject:
382
+ pass
383
+
384
+ @abstractmethod
385
+ def _upload(
386
+ self,
387
+ content: Union[str, bytes, TextIO, BinaryIO],
388
+ path: PathLike,
389
+ *,
390
+ overwrite: bool = False,
391
+ ) -> FilesObject:
392
+ pass
393
+
394
+ @abstractmethod
395
+ def mkdir(self, path: PathLike, overwrite: bool = False) -> FilesObject:
396
+ pass
397
+
398
+ @abstractmethod
399
+ def rename(
400
+ self,
401
+ old_path: PathLike,
402
+ new_path: PathLike,
403
+ *,
404
+ overwrite: bool = False,
405
+ ) -> FilesObject:
406
+ pass
407
+
408
+ @abstractmethod
409
+ def info(self, path: PathLike) -> FilesObject:
410
+ pass
411
+
412
+ @abstractmethod
413
+ def exists(self, path: PathLike) -> bool:
414
+ pass
415
+
416
+ @abstractmethod
417
+ def is_dir(self, path: PathLike) -> bool:
418
+ pass
419
+
420
+ @abstractmethod
421
+ def is_file(self, path: PathLike) -> bool:
422
+ pass
423
+
424
+ @abstractmethod
425
+ def listdir(
426
+ self,
427
+ path: PathLike = '/',
428
+ *,
429
+ recursive: bool = False,
430
+ ) -> List[str]:
431
+ pass
432
+
433
+ @abstractmethod
434
+ def download_file(
435
+ self,
436
+ path: PathLike,
437
+ local_path: Optional[PathLike] = None,
438
+ *,
439
+ overwrite: bool = False,
440
+ encoding: Optional[str] = None,
441
+ ) -> Optional[Union[bytes, str]]:
442
+ pass
443
+
444
+ @abstractmethod
445
+ def download_folder(
446
+ self,
447
+ path: PathLike,
448
+ local_path: PathLike = '.',
449
+ *,
450
+ overwrite: bool = False,
451
+ ) -> None:
452
+ pass
453
+
454
+ @abstractmethod
455
+ def remove(self, path: PathLike) -> None:
456
+ pass
457
+
458
+ @abstractmethod
459
+ def removedirs(self, path: PathLike) -> None:
460
+ pass
461
+
462
+ @abstractmethod
463
+ def rmdir(self, path: PathLike) -> None:
464
+ pass
465
+
466
+ @abstractmethod
467
+ def __str__(self) -> str:
468
+ pass
469
+
470
+ @abstractmethod
471
+ def __repr__(self) -> str:
472
+ pass
473
+
474
+
475
+ class FilesManager(Manager):
476
+ """
477
+ SingleStoreDB files manager.
478
+
479
+ This class should be instantiated using :func:`singlestoredb.manage_files`.
480
+
481
+ Parameters
482
+ ----------
483
+ access_token : str, optional
484
+ The API key or other access token for the files management API
485
+ version : str, optional
486
+ Version of the API to use
487
+ base_url : str, optional
488
+ Base URL of the files management API
489
+
490
+ See Also
491
+ --------
492
+ :func:`singlestoredb.manage_files`
493
+
494
+ """
495
+
496
+ #: Management API version if none is specified.
497
+ default_version = config.get_option('management.version')
498
+
499
+ #: Base URL if none is specified.
500
+ default_base_url = config.get_option('management.base_url')
501
+
502
+ #: Object type
503
+ obj_type = 'file'
504
+
505
+ @property
506
+ def personal_space(self) -> FileSpace:
507
+ """Return the personal file space."""
508
+ return FileSpace(PERSONAL_SPACE, self)
509
+
510
+ @property
511
+ def shared_space(self) -> FileSpace:
512
+ """Return the shared file space."""
513
+ return FileSpace(SHARED_SPACE, self)
514
+
515
+
516
+ def manage_files(
517
+ access_token: Optional[str] = None,
518
+ version: Optional[str] = None,
519
+ base_url: Optional[str] = None,
520
+ *,
521
+ organization_id: Optional[str] = None,
522
+ ) -> FilesManager:
523
+ """
524
+ Retrieve a SingleStoreDB files manager.
525
+
526
+ Parameters
527
+ ----------
528
+ access_token : str, optional
529
+ The API key or other access token for the files management API
530
+ version : str, optional
531
+ Version of the API to use
532
+ base_url : str, optional
533
+ Base URL of the files management API
534
+ organization_id : str, optional
535
+ ID of organization, if using a JWT for authentication
536
+
537
+ Returns
538
+ -------
539
+ :class:`FilesManager`
540
+
541
+ """
542
+ return FilesManager(
543
+ access_token=access_token, base_url=base_url,
544
+ version=version, organization_id=organization_id,
545
+ )
546
+
547
+
548
+ class FileSpace(FileLocation):
549
+ """
550
+ FileSpace manager.
551
+
552
+ This object is not instantiated directly.
553
+ It is returned by ``FilesManager.personal_space`` or ``FilesManager.shared_space``.
554
+
555
+ """
556
+
557
+ def __init__(self, location: str, manager: FilesManager):
558
+ self._location = location
559
+ self._manager = manager
560
+
561
+ def open(
562
+ self,
563
+ path: PathLike,
564
+ mode: str = 'r',
565
+ encoding: Optional[str] = None,
566
+ ) -> Union[io.StringIO, io.BytesIO]:
567
+ """
568
+ Open a file path for reading or writing.
569
+
570
+ Parameters
571
+ ----------
572
+ path : Path or str
573
+ The file path to read / write
574
+ mode : str, optional
575
+ The read / write mode. The following modes are supported:
576
+ * 'r' open for reading (default)
577
+ * 'w' open for writing, truncating the file first
578
+ * 'x' create a new file and open it for writing
579
+ The data type can be specified by adding one of the following:
580
+ * 'b' binary mode
581
+ * 't' text mode (default)
582
+ encoding : str, optional
583
+ The string encoding to use for text
584
+
585
+ Returns
586
+ -------
587
+ FilesObjectBytesReader - 'rb' or 'b' mode
588
+ FilesObjectBytesWriter - 'wb' or 'xb' mode
589
+ FilesObjectTextReader - 'r' or 'rt' mode
590
+ FilesObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode
591
+
592
+ """
593
+ if '+' in mode or 'a' in mode:
594
+ raise ManagementError(msg='modifying an existing file is not supported')
595
+
596
+ if 'w' in mode or 'x' in mode:
597
+ exists = self.exists(path)
598
+ if exists:
599
+ if 'x' in mode:
600
+ raise FileExistsError(f'file path already exists: {path}')
601
+ self.remove(path)
602
+ if 'b' in mode:
603
+ return FilesObjectBytesWriter(b'', self, path)
604
+ return FilesObjectTextWriter('', self, path)
605
+
606
+ if 'r' in mode:
607
+ content = self.download_file(path)
608
+ if isinstance(content, bytes):
609
+ if 'b' in mode:
610
+ return FilesObjectBytesReader(content)
611
+ encoding = 'utf-8' if encoding is None else encoding
612
+ return FilesObjectTextReader(content.decode(encoding))
613
+
614
+ if isinstance(content, str):
615
+ return FilesObjectTextReader(content)
616
+
617
+ raise ValueError(f'unrecognized file content type: {type(content)}')
618
+
619
+ raise ValueError(f'must have one of create/read/write mode specified: {mode}')
620
+
621
+ def upload_file(
622
+ self,
623
+ local_path: Union[PathLike, TextIO, BinaryIO],
624
+ path: PathLike,
625
+ *,
626
+ overwrite: bool = False,
627
+ ) -> FilesObject:
628
+ """
629
+ Upload a local file.
630
+
631
+ Parameters
632
+ ----------
633
+ local_path : Path or str or file-like
634
+ Path to the local file or an open file object
635
+ path : Path or str
636
+ Path to the file
637
+ overwrite : bool, optional
638
+ Should the ``path`` be overwritten if it exists already?
639
+
640
+ """
641
+ if isinstance(local_path, (TextIO, BinaryIO)):
642
+ pass
643
+ elif not os.path.isfile(local_path):
644
+ raise IsADirectoryError(f'local path is not a file: {local_path}')
645
+
646
+ if self.exists(path):
647
+ if not overwrite:
648
+ raise OSError(f'file path already exists: {path}')
649
+
650
+ self.remove(path)
651
+
652
+ if isinstance(local_path, (TextIO, BinaryIO)):
653
+ return self._upload(local_path, path, overwrite=overwrite)
654
+ return self._upload(open(local_path, 'rb'), path, overwrite=overwrite)
655
+
656
+ def upload_folder(
657
+ self,
658
+ local_path: PathLike,
659
+ path: PathLike,
660
+ *,
661
+ overwrite: bool = False,
662
+ recursive: bool = True,
663
+ include_root: bool = False,
664
+ ignore: Optional[Union[PathLike, List[PathLike]]] = None,
665
+ ) -> FilesObject:
666
+ """
667
+ Upload a folder recursively.
668
+
669
+ Only the contents of the folder are uploaded. To include the
670
+ folder name itself in the target path use ``include_root=True``.
671
+
672
+ Parameters
673
+ ----------
674
+ local_path : Path or str
675
+ Local directory to upload
676
+ path : Path or str
677
+ Path of folder to upload to
678
+ overwrite : bool, optional
679
+ If a file already exists, should it be overwritten?
680
+ recursive : bool, optional
681
+ Should nested folders be uploaded?
682
+ include_root : bool, optional
683
+ Should the local root folder itself be uploaded as the top folder?
684
+ ignore : Path or str or List[Path] or List[str], optional
685
+ Glob patterns of files to ignore, for example, '**/*.pyc` will
686
+ ignore all '*.pyc' files in the directory tree
687
+
688
+ """
689
+ raise ManagementError(
690
+ msg='Operation not supported: directories are currently not allowed '
691
+ 'in Files API',
692
+ )
693
+
694
+ def _upload(
695
+ self,
696
+ content: Union[str, bytes, TextIO, BinaryIO],
697
+ path: PathLike,
698
+ *,
699
+ overwrite: bool = False,
700
+ ) -> FilesObject:
701
+ """
702
+ Upload content to a file.
703
+
704
+ Parameters
705
+ ----------
706
+ content : str or bytes or file-like
707
+ Content to upload
708
+ path : Path or str
709
+ Path to the file
710
+ overwrite : bool, optional
711
+ Should the ``path`` be overwritten if it exists already?
712
+
713
+ """
714
+ if self.exists(path):
715
+ if not overwrite:
716
+ raise OSError(f'file path already exists: {path}')
717
+ self.remove(path)
718
+
719
+ self._manager._put(
720
+ f'files/fs/{self._location}/{path}',
721
+ files={'file': content},
722
+ headers={'Content-Type': None},
723
+ )
724
+
725
+ return self.info(path)
726
+
727
+ def mkdir(self, path: PathLike, overwrite: bool = False) -> FilesObject:
728
+ """
729
+ Make a directory in the file space.
730
+
731
+ Parameters
732
+ ----------
733
+ path : Path or str
734
+ Path of the folder to create
735
+ overwrite : bool, optional
736
+ Should the file path be overwritten if it exists already?
737
+
738
+ Returns
739
+ -------
740
+ FilesObject
741
+
742
+ """
743
+ raise ManagementError(
744
+ msg='Operation not supported: directories are currently not allowed '
745
+ 'in Files API',
746
+ )
747
+
748
+ mkdirs = mkdir
749
+
750
+ def rename(
751
+ self,
752
+ old_path: PathLike,
753
+ new_path: PathLike,
754
+ *,
755
+ overwrite: bool = False,
756
+ ) -> FilesObject:
757
+ """
758
+ Move the file to a new location.
759
+
760
+ Parameters
761
+ -----------
762
+ old_path : Path or str
763
+ Original location of the path
764
+ new_path : Path or str
765
+ New location of the path
766
+ overwrite : bool, optional
767
+ Should the ``new_path`` be overwritten if it exists already?
768
+
769
+ """
770
+ if not self.exists(old_path):
771
+ raise OSError(f'file path does not exist: {old_path}')
772
+
773
+ if str(old_path).endswith('/') or str(new_path).endswith('/'):
774
+ raise ManagementError(
775
+ msg='Operation not supported: directories are currently not allowed '
776
+ 'in Files API',
777
+ )
778
+
779
+ if self.exists(new_path):
780
+ if not overwrite:
781
+ raise OSError(f'file path already exists: {new_path}')
782
+
783
+ self.remove(new_path)
784
+
785
+ self._manager._patch(
786
+ f'files/fs/{self._location}/{old_path}',
787
+ json=dict(newPath=new_path),
788
+ )
789
+
790
+ return self.info(new_path)
791
+
792
+ def info(self, path: PathLike) -> FilesObject:
793
+ """
794
+ Return information about a file location.
795
+
796
+ Parameters
797
+ ----------
798
+ path : Path or str
799
+ Path to the file
800
+
801
+ Returns
802
+ -------
803
+ FilesObject
804
+
805
+ """
806
+ res = self._manager._get(
807
+ re.sub(r'/+$', r'/', f'files/fs/{self._location}/{path}'),
808
+ params=dict(metadata=1),
809
+ ).json()
810
+
811
+ return FilesObject.from_dict(res, self)
812
+
813
+ def exists(self, path: PathLike) -> bool:
814
+ """
815
+ Does the given file path exist?
816
+
817
+ Parameters
818
+ ----------
819
+ path : Path or str
820
+ Path to file object
821
+
822
+ Returns
823
+ -------
824
+ bool
825
+
826
+ """
827
+ try:
828
+ self.info(path)
829
+ return True
830
+ except ManagementError as exc:
831
+ if exc.errno == 404:
832
+ return False
833
+ raise
834
+
835
+ def is_dir(self, path: PathLike) -> bool:
836
+ """
837
+ Is the given file path a directory?
838
+
839
+ Parameters
840
+ ----------
841
+ path : Path or str
842
+ Path to file object
843
+
844
+ Returns
845
+ -------
846
+ bool
847
+
848
+ """
849
+ try:
850
+ return self.info(path).type == 'directory'
851
+ except ManagementError as exc:
852
+ if exc.errno == 404:
853
+ return False
854
+ raise
855
+
856
+ def is_file(self, path: PathLike) -> bool:
857
+ """
858
+ Is the given file path a file?
859
+
860
+ Parameters
861
+ ----------
862
+ path : Path or str
863
+ Path to file object
864
+
865
+ Returns
866
+ -------
867
+ bool
868
+
869
+ """
870
+ try:
871
+ return self.info(path).type != 'directory'
872
+ except ManagementError as exc:
873
+ if exc.errno == 404:
874
+ return False
875
+ raise
876
+
877
+ def _list_root_dir(self) -> List[str]:
878
+ """
879
+ Return the names of files in the root directory.
880
+ Parameters
881
+ ----------
882
+ """
883
+ res = self._manager._get(
884
+ f'files/fs/{self._location}',
885
+ ).json()
886
+ return [x['path'] for x in res['content'] or []]
887
+
888
+ def listdir(
889
+ self,
890
+ path: PathLike = '/',
891
+ *,
892
+ recursive: bool = False,
893
+ ) -> List[str]:
894
+ """
895
+ List the files / folders at the given path.
896
+
897
+ Parameters
898
+ ----------
899
+ path : Path or str, optional
900
+ Path to the file location
901
+
902
+ Returns
903
+ -------
904
+ List[str]
905
+
906
+ """
907
+ if path == '' or path == '/':
908
+ return self._list_root_dir()
909
+
910
+ raise ManagementError(
911
+ msg='Operation not supported: directories are currently not allowed '
912
+ 'in Files API',
913
+ )
914
+
915
+ def download_file(
916
+ self,
917
+ path: PathLike,
918
+ local_path: Optional[PathLike] = None,
919
+ *,
920
+ overwrite: bool = False,
921
+ encoding: Optional[str] = None,
922
+ ) -> Optional[Union[bytes, str]]:
923
+ """
924
+ Download the content of a file path.
925
+
926
+ Parameters
927
+ ----------
928
+ path : Path or str
929
+ Path to the file
930
+ local_path : Path or str
931
+ Path to local file target location
932
+ overwrite : bool, optional
933
+ Should an existing file be overwritten if it exists?
934
+ encoding : str, optional
935
+ Encoding used to convert the resulting data
936
+
937
+ Returns
938
+ -------
939
+ bytes or str - ``local_path`` is None
940
+ None - ``local_path`` is a Path or str
941
+
942
+ """
943
+ if local_path is not None and not overwrite and os.path.exists(local_path):
944
+ raise OSError('target file already exists; use overwrite=True to replace')
945
+ if self.is_dir(path):
946
+ raise IsADirectoryError(f'file path is a directory: {path}')
947
+
948
+ out = self._manager._get(
949
+ f'files/fs/{self._location}/{path}',
950
+ ).content
951
+
952
+ if local_path is not None:
953
+ with open(local_path, 'wb') as outfile:
954
+ outfile.write(out)
955
+ return None
956
+
957
+ if encoding:
958
+ return out.decode(encoding)
959
+
960
+ return out
961
+
962
+ def download_folder(
963
+ self,
964
+ path: PathLike,
965
+ local_path: PathLike = '.',
966
+ *,
967
+ overwrite: bool = False,
968
+ ) -> None:
969
+ """
970
+ Download a FileSpace folder to a local directory.
971
+
972
+ Parameters
973
+ ----------
974
+ path : Path or str
975
+ Path to the file
976
+ local_path : Path or str
977
+ Path to local directory target location
978
+ overwrite : bool, optional
979
+ Should an existing directory / files be overwritten if they exist?
980
+
981
+ """
982
+ raise ManagementError(
983
+ msg='Operation not supported: directories are currently not allowed '
984
+ 'in Files API',
985
+ )
986
+
987
+ def remove(self, path: PathLike) -> None:
988
+ """
989
+ Delete a file location.
990
+
991
+ Parameters
992
+ ----------
993
+ path : Path or str
994
+ Path to the location
995
+
996
+ """
997
+ if self.is_dir(path):
998
+ raise IsADirectoryError('file path is a directory')
999
+
1000
+ self._manager._delete(f'files/fs/{self._location}/{path}')
1001
+
1002
+ def removedirs(self, path: PathLike) -> None:
1003
+ """
1004
+ Delete a folder recursively.
1005
+
1006
+ Parameters
1007
+ ----------
1008
+ path : Path or str
1009
+ Path to the file location
1010
+
1011
+ """
1012
+ raise ManagementError(
1013
+ msg='Operation not supported: directories are currently not allowed '
1014
+ 'in Files API',
1015
+ )
1016
+
1017
+ def rmdir(self, path: PathLike) -> None:
1018
+ """
1019
+ Delete a folder.
1020
+
1021
+ Parameters
1022
+ ----------
1023
+ path : Path or str
1024
+ Path to the file location
1025
+
1026
+ """
1027
+ raise ManagementError(
1028
+ msg='Operation not supported: directories are currently not allowed '
1029
+ 'in Files API',
1030
+ )
1031
+
1032
+ def __str__(self) -> str:
1033
+ """Return string representation."""
1034
+ return vars_to_str(self)
1035
+
1036
+ def __repr__(self) -> str:
1037
+ """Return string representation."""
1038
+ return str(self)