singlestoredb 1.8.0__cp38-abi3-win32.whl → 1.10.0__cp38-abi3-win32.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,1039 @@
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') or 'v1'
498
+
499
+ #: Base URL if none is specified.
500
+ default_base_url = config.get_option('management.base_url') \
501
+ or 'https://api.singlestore.com'
502
+
503
+ #: Object type
504
+ obj_type = 'file'
505
+
506
+ @property
507
+ def personal_space(self) -> FileSpace:
508
+ """Return the personal file space."""
509
+ return FileSpace(PERSONAL_SPACE, self)
510
+
511
+ @property
512
+ def shared_space(self) -> FileSpace:
513
+ """Return the shared file space."""
514
+ return FileSpace(SHARED_SPACE, self)
515
+
516
+
517
+ def manage_files(
518
+ access_token: Optional[str] = None,
519
+ version: Optional[str] = None,
520
+ base_url: Optional[str] = None,
521
+ *,
522
+ organization_id: Optional[str] = None,
523
+ ) -> FilesManager:
524
+ """
525
+ Retrieve a SingleStoreDB files manager.
526
+
527
+ Parameters
528
+ ----------
529
+ access_token : str, optional
530
+ The API key or other access token for the files management API
531
+ version : str, optional
532
+ Version of the API to use
533
+ base_url : str, optional
534
+ Base URL of the files management API
535
+ organization_id : str, optional
536
+ ID of organization, if using a JWT for authentication
537
+
538
+ Returns
539
+ -------
540
+ :class:`FilesManager`
541
+
542
+ """
543
+ return FilesManager(
544
+ access_token=access_token, base_url=base_url,
545
+ version=version, organization_id=organization_id,
546
+ )
547
+
548
+
549
+ class FileSpace(FileLocation):
550
+ """
551
+ FileSpace manager.
552
+
553
+ This object is not instantiated directly.
554
+ It is returned by ``FilesManager.personal_space`` or ``FilesManager.shared_space``.
555
+
556
+ """
557
+
558
+ def __init__(self, location: str, manager: FilesManager):
559
+ self._location = location
560
+ self._manager = manager
561
+
562
+ def open(
563
+ self,
564
+ path: PathLike,
565
+ mode: str = 'r',
566
+ encoding: Optional[str] = None,
567
+ ) -> Union[io.StringIO, io.BytesIO]:
568
+ """
569
+ Open a file path for reading or writing.
570
+
571
+ Parameters
572
+ ----------
573
+ path : Path or str
574
+ The file path to read / write
575
+ mode : str, optional
576
+ The read / write mode. The following modes are supported:
577
+ * 'r' open for reading (default)
578
+ * 'w' open for writing, truncating the file first
579
+ * 'x' create a new file and open it for writing
580
+ The data type can be specified by adding one of the following:
581
+ * 'b' binary mode
582
+ * 't' text mode (default)
583
+ encoding : str, optional
584
+ The string encoding to use for text
585
+
586
+ Returns
587
+ -------
588
+ FilesObjectBytesReader - 'rb' or 'b' mode
589
+ FilesObjectBytesWriter - 'wb' or 'xb' mode
590
+ FilesObjectTextReader - 'r' or 'rt' mode
591
+ FilesObjectTextWriter - 'w', 'x', 'wt' or 'xt' mode
592
+
593
+ """
594
+ if '+' in mode or 'a' in mode:
595
+ raise ManagementError(msg='modifying an existing file is not supported')
596
+
597
+ if 'w' in mode or 'x' in mode:
598
+ exists = self.exists(path)
599
+ if exists:
600
+ if 'x' in mode:
601
+ raise FileExistsError(f'file path already exists: {path}')
602
+ self.remove(path)
603
+ if 'b' in mode:
604
+ return FilesObjectBytesWriter(b'', self, path)
605
+ return FilesObjectTextWriter('', self, path)
606
+
607
+ if 'r' in mode:
608
+ content = self.download_file(path)
609
+ if isinstance(content, bytes):
610
+ if 'b' in mode:
611
+ return FilesObjectBytesReader(content)
612
+ encoding = 'utf-8' if encoding is None else encoding
613
+ return FilesObjectTextReader(content.decode(encoding))
614
+
615
+ if isinstance(content, str):
616
+ return FilesObjectTextReader(content)
617
+
618
+ raise ValueError(f'unrecognized file content type: {type(content)}')
619
+
620
+ raise ValueError(f'must have one of create/read/write mode specified: {mode}')
621
+
622
+ def upload_file(
623
+ self,
624
+ local_path: Union[PathLike, TextIO, BinaryIO],
625
+ path: PathLike,
626
+ *,
627
+ overwrite: bool = False,
628
+ ) -> FilesObject:
629
+ """
630
+ Upload a local file.
631
+
632
+ Parameters
633
+ ----------
634
+ local_path : Path or str or file-like
635
+ Path to the local file or an open file object
636
+ path : Path or str
637
+ Path to the file
638
+ overwrite : bool, optional
639
+ Should the ``path`` be overwritten if it exists already?
640
+
641
+ """
642
+ if isinstance(local_path, (TextIO, BinaryIO)):
643
+ pass
644
+ elif not os.path.isfile(local_path):
645
+ raise IsADirectoryError(f'local path is not a file: {local_path}')
646
+
647
+ if self.exists(path):
648
+ if not overwrite:
649
+ raise OSError(f'file path already exists: {path}')
650
+
651
+ self.remove(path)
652
+
653
+ if isinstance(local_path, (TextIO, BinaryIO)):
654
+ return self._upload(local_path, path, overwrite=overwrite)
655
+ return self._upload(open(local_path, 'rb'), path, overwrite=overwrite)
656
+
657
+ def upload_folder(
658
+ self,
659
+ local_path: PathLike,
660
+ path: PathLike,
661
+ *,
662
+ overwrite: bool = False,
663
+ recursive: bool = True,
664
+ include_root: bool = False,
665
+ ignore: Optional[Union[PathLike, List[PathLike]]] = None,
666
+ ) -> FilesObject:
667
+ """
668
+ Upload a folder recursively.
669
+
670
+ Only the contents of the folder are uploaded. To include the
671
+ folder name itself in the target path use ``include_root=True``.
672
+
673
+ Parameters
674
+ ----------
675
+ local_path : Path or str
676
+ Local directory to upload
677
+ path : Path or str
678
+ Path of folder to upload to
679
+ overwrite : bool, optional
680
+ If a file already exists, should it be overwritten?
681
+ recursive : bool, optional
682
+ Should nested folders be uploaded?
683
+ include_root : bool, optional
684
+ Should the local root folder itself be uploaded as the top folder?
685
+ ignore : Path or str or List[Path] or List[str], optional
686
+ Glob patterns of files to ignore, for example, '**/*.pyc` will
687
+ ignore all '*.pyc' files in the directory tree
688
+
689
+ """
690
+ raise ManagementError(
691
+ msg='Operation not supported: directories are currently not allowed '
692
+ 'in Files API',
693
+ )
694
+
695
+ def _upload(
696
+ self,
697
+ content: Union[str, bytes, TextIO, BinaryIO],
698
+ path: PathLike,
699
+ *,
700
+ overwrite: bool = False,
701
+ ) -> FilesObject:
702
+ """
703
+ Upload content to a file.
704
+
705
+ Parameters
706
+ ----------
707
+ content : str or bytes or file-like
708
+ Content to upload
709
+ path : Path or str
710
+ Path to the file
711
+ overwrite : bool, optional
712
+ Should the ``path`` be overwritten if it exists already?
713
+
714
+ """
715
+ if self.exists(path):
716
+ if not overwrite:
717
+ raise OSError(f'file path already exists: {path}')
718
+ self.remove(path)
719
+
720
+ self._manager._put(
721
+ f'files/fs/{self._location}/{path}',
722
+ files={'file': content},
723
+ headers={'Content-Type': None},
724
+ )
725
+
726
+ return self.info(path)
727
+
728
+ def mkdir(self, path: PathLike, overwrite: bool = False) -> FilesObject:
729
+ """
730
+ Make a directory in the file space.
731
+
732
+ Parameters
733
+ ----------
734
+ path : Path or str
735
+ Path of the folder to create
736
+ overwrite : bool, optional
737
+ Should the file path be overwritten if it exists already?
738
+
739
+ Returns
740
+ -------
741
+ FilesObject
742
+
743
+ """
744
+ raise ManagementError(
745
+ msg='Operation not supported: directories are currently not allowed '
746
+ 'in Files API',
747
+ )
748
+
749
+ mkdirs = mkdir
750
+
751
+ def rename(
752
+ self,
753
+ old_path: PathLike,
754
+ new_path: PathLike,
755
+ *,
756
+ overwrite: bool = False,
757
+ ) -> FilesObject:
758
+ """
759
+ Move the file to a new location.
760
+
761
+ Parameters
762
+ -----------
763
+ old_path : Path or str
764
+ Original location of the path
765
+ new_path : Path or str
766
+ New location of the path
767
+ overwrite : bool, optional
768
+ Should the ``new_path`` be overwritten if it exists already?
769
+
770
+ """
771
+ if not self.exists(old_path):
772
+ raise OSError(f'file path does not exist: {old_path}')
773
+
774
+ if str(old_path).endswith('/') or str(new_path).endswith('/'):
775
+ raise ManagementError(
776
+ msg='Operation not supported: directories are currently not allowed '
777
+ 'in Files API',
778
+ )
779
+
780
+ if self.exists(new_path):
781
+ if not overwrite:
782
+ raise OSError(f'file path already exists: {new_path}')
783
+
784
+ self.remove(new_path)
785
+
786
+ self._manager._patch(
787
+ f'files/fs/{self._location}/{old_path}',
788
+ json=dict(newPath=new_path),
789
+ )
790
+
791
+ return self.info(new_path)
792
+
793
+ def info(self, path: PathLike) -> FilesObject:
794
+ """
795
+ Return information about a file location.
796
+
797
+ Parameters
798
+ ----------
799
+ path : Path or str
800
+ Path to the file
801
+
802
+ Returns
803
+ -------
804
+ FilesObject
805
+
806
+ """
807
+ res = self._manager._get(
808
+ re.sub(r'/+$', r'/', f'files/fs/{self._location}/{path}'),
809
+ params=dict(metadata=1),
810
+ ).json()
811
+
812
+ return FilesObject.from_dict(res, self)
813
+
814
+ def exists(self, path: PathLike) -> bool:
815
+ """
816
+ Does the given file path exist?
817
+
818
+ Parameters
819
+ ----------
820
+ path : Path or str
821
+ Path to file object
822
+
823
+ Returns
824
+ -------
825
+ bool
826
+
827
+ """
828
+ try:
829
+ self.info(path)
830
+ return True
831
+ except ManagementError as exc:
832
+ if exc.errno == 404:
833
+ return False
834
+ raise
835
+
836
+ def is_dir(self, path: PathLike) -> bool:
837
+ """
838
+ Is the given file path a directory?
839
+
840
+ Parameters
841
+ ----------
842
+ path : Path or str
843
+ Path to file object
844
+
845
+ Returns
846
+ -------
847
+ bool
848
+
849
+ """
850
+ try:
851
+ return self.info(path).type == 'directory'
852
+ except ManagementError as exc:
853
+ if exc.errno == 404:
854
+ return False
855
+ raise
856
+
857
+ def is_file(self, path: PathLike) -> bool:
858
+ """
859
+ Is the given file path a file?
860
+
861
+ Parameters
862
+ ----------
863
+ path : Path or str
864
+ Path to file object
865
+
866
+ Returns
867
+ -------
868
+ bool
869
+
870
+ """
871
+ try:
872
+ return self.info(path).type != 'directory'
873
+ except ManagementError as exc:
874
+ if exc.errno == 404:
875
+ return False
876
+ raise
877
+
878
+ def _list_root_dir(self) -> List[str]:
879
+ """
880
+ Return the names of files in the root directory.
881
+ Parameters
882
+ ----------
883
+ """
884
+ res = self._manager._get(
885
+ f'files/fs/{self._location}',
886
+ ).json()
887
+ return [x['path'] for x in res['content'] or []]
888
+
889
+ def listdir(
890
+ self,
891
+ path: PathLike = '/',
892
+ *,
893
+ recursive: bool = False,
894
+ ) -> List[str]:
895
+ """
896
+ List the files / folders at the given path.
897
+
898
+ Parameters
899
+ ----------
900
+ path : Path or str, optional
901
+ Path to the file location
902
+
903
+ Returns
904
+ -------
905
+ List[str]
906
+
907
+ """
908
+ if path == '' or path == '/':
909
+ return self._list_root_dir()
910
+
911
+ raise ManagementError(
912
+ msg='Operation not supported: directories are currently not allowed '
913
+ 'in Files API',
914
+ )
915
+
916
+ def download_file(
917
+ self,
918
+ path: PathLike,
919
+ local_path: Optional[PathLike] = None,
920
+ *,
921
+ overwrite: bool = False,
922
+ encoding: Optional[str] = None,
923
+ ) -> Optional[Union[bytes, str]]:
924
+ """
925
+ Download the content of a file path.
926
+
927
+ Parameters
928
+ ----------
929
+ path : Path or str
930
+ Path to the file
931
+ local_path : Path or str
932
+ Path to local file target location
933
+ overwrite : bool, optional
934
+ Should an existing file be overwritten if it exists?
935
+ encoding : str, optional
936
+ Encoding used to convert the resulting data
937
+
938
+ Returns
939
+ -------
940
+ bytes or str - ``local_path`` is None
941
+ None - ``local_path`` is a Path or str
942
+
943
+ """
944
+ if local_path is not None and not overwrite and os.path.exists(local_path):
945
+ raise OSError('target file already exists; use overwrite=True to replace')
946
+ if self.is_dir(path):
947
+ raise IsADirectoryError(f'file path is a directory: {path}')
948
+
949
+ out = self._manager._get(
950
+ f'files/fs/{self._location}/{path}',
951
+ ).content
952
+
953
+ if local_path is not None:
954
+ with open(local_path, 'wb') as outfile:
955
+ outfile.write(out)
956
+ return None
957
+
958
+ if encoding:
959
+ return out.decode(encoding)
960
+
961
+ return out
962
+
963
+ def download_folder(
964
+ self,
965
+ path: PathLike,
966
+ local_path: PathLike = '.',
967
+ *,
968
+ overwrite: bool = False,
969
+ ) -> None:
970
+ """
971
+ Download a FileSpace folder to a local directory.
972
+
973
+ Parameters
974
+ ----------
975
+ path : Path or str
976
+ Path to the file
977
+ local_path : Path or str
978
+ Path to local directory target location
979
+ overwrite : bool, optional
980
+ Should an existing directory / files be overwritten if they exist?
981
+
982
+ """
983
+ raise ManagementError(
984
+ msg='Operation not supported: directories are currently not allowed '
985
+ 'in Files API',
986
+ )
987
+
988
+ def remove(self, path: PathLike) -> None:
989
+ """
990
+ Delete a file location.
991
+
992
+ Parameters
993
+ ----------
994
+ path : Path or str
995
+ Path to the location
996
+
997
+ """
998
+ if self.is_dir(path):
999
+ raise IsADirectoryError('file path is a directory')
1000
+
1001
+ self._manager._delete(f'files/fs/{self._location}/{path}')
1002
+
1003
+ def removedirs(self, path: PathLike) -> None:
1004
+ """
1005
+ Delete a folder recursively.
1006
+
1007
+ Parameters
1008
+ ----------
1009
+ path : Path or str
1010
+ Path to the file location
1011
+
1012
+ """
1013
+ raise ManagementError(
1014
+ msg='Operation not supported: directories are currently not allowed '
1015
+ 'in Files API',
1016
+ )
1017
+
1018
+ def rmdir(self, path: PathLike) -> None:
1019
+ """
1020
+ Delete a folder.
1021
+
1022
+ Parameters
1023
+ ----------
1024
+ path : Path or str
1025
+ Path to the file location
1026
+
1027
+ """
1028
+ raise ManagementError(
1029
+ msg='Operation not supported: directories are currently not allowed '
1030
+ 'in Files API',
1031
+ )
1032
+
1033
+ def __str__(self) -> str:
1034
+ """Return string representation."""
1035
+ return vars_to_str(self)
1036
+
1037
+ def __repr__(self) -> str:
1038
+ """Return string representation."""
1039
+ return str(self)