datatypes 0.31.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.
Files changed (53) hide show
  1. datatypes/__init__.py +169 -0
  2. datatypes/collections/__init__.py +25 -0
  3. datatypes/collections/container.py +248 -0
  4. datatypes/collections/mapping.py +1226 -0
  5. datatypes/collections/sequence.py +311 -0
  6. datatypes/command.py +596 -0
  7. datatypes/compat.py +206 -0
  8. datatypes/config/__init__.py +13 -0
  9. datatypes/config/environ/__init__.py +33 -0
  10. datatypes/config/environ/base.py +332 -0
  11. datatypes/config/environ/token.py +173 -0
  12. datatypes/config/parser.py +256 -0
  13. datatypes/config/settings.py +352 -0
  14. datatypes/copy.py +151 -0
  15. datatypes/csv.py +547 -0
  16. datatypes/datetime.py +1243 -0
  17. datatypes/decorators/__init__.py +31 -0
  18. datatypes/decorators/base.py +449 -0
  19. datatypes/decorators/descriptor.py +490 -0
  20. datatypes/decorators/misc.py +152 -0
  21. datatypes/email.py +439 -0
  22. datatypes/enum.py +176 -0
  23. datatypes/event.py +299 -0
  24. datatypes/exception.py +13 -0
  25. datatypes/filetypes/__init__.py +10 -0
  26. datatypes/filetypes/html.py +540 -0
  27. datatypes/filetypes/toml.py +275 -0
  28. datatypes/http.py +1443 -0
  29. datatypes/logging.py +475 -0
  30. datatypes/number.py +586 -0
  31. datatypes/parse.py +362 -0
  32. datatypes/path.py +4358 -0
  33. datatypes/profile.py +246 -0
  34. datatypes/reflection/__init__.py +24 -0
  35. datatypes/reflection/inspect.py +3475 -0
  36. datatypes/reflection/other.py +843 -0
  37. datatypes/reflection/path.py +639 -0
  38. datatypes/server.py +642 -0
  39. datatypes/service.py +212 -0
  40. datatypes/ssh.py +812 -0
  41. datatypes/string.py +1632 -0
  42. datatypes/token/__init__.py +16 -0
  43. datatypes/token/abnf.py +1696 -0
  44. datatypes/token/base.py +921 -0
  45. datatypes/token/word.py +259 -0
  46. datatypes/typing.py +126 -0
  47. datatypes/url.py +988 -0
  48. datatypes/utils.py +179 -0
  49. datatypes-0.31.0.dist-info/METADATA +38 -0
  50. datatypes-0.31.0.dist-info/RECORD +53 -0
  51. datatypes-0.31.0.dist-info/WHEEL +5 -0
  52. datatypes-0.31.0.dist-info/licenses/LICENSE.txt +21 -0
  53. datatypes-0.31.0.dist-info/top_level.txt +1 -0
datatypes/csv.py ADDED
@@ -0,0 +1,547 @@
1
+ # -*- coding: utf-8 -*-
2
+ import csv
3
+ from contextlib import contextmanager
4
+ import io
5
+
6
+ from .compat import *
7
+ from .config.environ import environ
8
+ from .string import String, ByteString
9
+ from .path import TempFilepath, Filepath
10
+ from . import logging
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CSVRowDict(dict):
17
+ """An actual dict for each row
18
+
19
+ While this is about 50% slower than CSVRow, sometimes, like trying to
20
+ serialize a CSV row using orjson, you need an actual dict and not
21
+ something that just quacks like a dict. Basically, anything that is
22
+ using the Python C api PyDict_Next won't be able to "see" the CSVRow's
23
+ actual data so you have to take the speed hit and use something like
24
+ this class
25
+
26
+ .. Example:
27
+ # use a custom CSV row class
28
+ c = CSV("<CSV-PATH>", reader_row_class=CSVRowDict)
29
+ for row in c:
30
+ assert isinstance(row, dict)
31
+ """
32
+ def __init__(self, columns, lookup):
33
+ super().__init__(zip(lookup.keys(), columns))
34
+
35
+
36
+ class CSVRow(Mapping):
37
+ """By default the CSV reader will return an instance of this class
38
+
39
+ This is done in `CSV.create_reader_row`. This exists because
40
+ csv.DictReader was quite a bit slower than expected in real world use,
41
+ using this sped up CSV reading by about 2.5-3x.
42
+
43
+ This walks and talks like a dict but it is not a traditional dict, that's
44
+ something to be aware of.
45
+
46
+ There is a bit of overhead for creating these instances, so if the most
47
+ performance is a necessity, you can override `CSV.create_reader_row` and
48
+ just return the columns, then you can manually grab columns using the
49
+ CSV instance's `.lookup` property exactly how this class does it
50
+
51
+ `Mapping` parent gives implementations for .__contains__, .items, .keys,
52
+ and .values
53
+
54
+ https://github.com/python/cpython/blob/3.11/Lib/_collections_abc.py#L754
55
+ """
56
+ def __init__(self, columns, lookup):
57
+ """
58
+ :param columns: list, the list of values of this row
59
+ :param lookup: dict[str, int], the keys are the field names and the
60
+ values are the index in `columns` of the value that corresponds
61
+ to that field name
62
+ """
63
+ self.columns = columns
64
+ self.lookup = lookup
65
+ self.mutable = False
66
+
67
+ def _make_mutable(self):
68
+ """Converts this row into a mutable row
69
+
70
+ If we're adding or removing fields then we need a local copy of
71
+ .lookup so we don't mess up any other rows in the CSV
72
+ """
73
+ if not self.mutable:
74
+ self.lookup = {**self.lookup}
75
+ self.mutable = True
76
+
77
+ def __getitem__(self, k):
78
+ return self.columns[self.lookup[k]]
79
+
80
+ def __setitem__(self, k, v):
81
+ """the value of .columns is updated with v if it already exists,
82
+ otherwise v is added to the end of .columns"""
83
+ if k in self.lookup:
84
+ self.columns[self.lookup[k]] = v
85
+
86
+ else:
87
+ self._make_mutable()
88
+ self.lookup[k] = len(self.columns)
89
+ self.columns.append(v)
90
+
91
+ def __delitem__(self, k):
92
+ """This just sets the field's value in .columns to None and removes k
93
+ from .lookup"""
94
+ self._make_mutable()
95
+ i = self.lookup.pop(k)
96
+ self.columns[i] = None
97
+
98
+ def __iter__(self):
99
+ if self.columns:
100
+ yield from self.lookup
101
+
102
+ def __len__(self):
103
+ return len(self.lookup)
104
+
105
+ def pop(self, k, *default):
106
+ try:
107
+ v = self[k]
108
+ del self[k]
109
+
110
+ except KeyError:
111
+ if default:
112
+ v = default[0]
113
+
114
+ else:
115
+ raise
116
+
117
+ return v
118
+
119
+ def __bool__(self):
120
+ return bool(self.columns)
121
+
122
+ def popitem(self):
123
+ self._make_mutable()
124
+ k, i = self.lookup.popitem()
125
+ v = self.columns[i]
126
+ self.columns[i] = None
127
+ return k, v
128
+
129
+ def clear(self):
130
+ self._make_mutable()
131
+ self.lookup.clear()
132
+ self.columns = []
133
+
134
+ def __reversed__(self):
135
+ return reversed(list(self.keys()))
136
+
137
+ def copy(self):
138
+ return type(self)(self.columns, self.lookup)
139
+
140
+ def setdefault(self, k, v):
141
+ if k not in self:
142
+ self[k] = v
143
+
144
+ def update(self, *others, **kwargs):
145
+ for other in others:
146
+ if keys := getattr(other, "keys", None):
147
+ for k in keys():
148
+ self[k] = other[k]
149
+
150
+ else:
151
+ for k, v in other:
152
+ self[k] = v
153
+
154
+ for k, v in kwargs.items():
155
+ self[k] = v
156
+
157
+ def __or__(self, other):
158
+ r = self.copy()
159
+ r.update(other)
160
+ return r
161
+
162
+ def __ior__(self, other):
163
+ self.update(other)
164
+ return self
165
+
166
+
167
+ class CSV(object):
168
+ """Easily read/write the rows of a csv file
169
+
170
+ :Example:
171
+ # read a csv file
172
+ c = CSV("/path/to/read/file.csv")
173
+ for row in c:
174
+ print(row)
175
+ # write a csv file
176
+ c = CSV("/path/to/write/file.csv")
177
+ with c:
178
+ for row in rows:
179
+ c.add(row)
180
+
181
+ In testing, this is about 25-30% slower than the vanilla CSV reader but
182
+ gives you dict support among other niceties
183
+
184
+ https://docs.python.org/3/library/csv.html
185
+ """
186
+ def __init__(self, path, fieldnames=None, encoding="", **kwargs):
187
+ """Create the csv instance
188
+ :param path: str|IOBase, the path to the csv file that will be
189
+ read/written :param fieldnames: list, the fieldnames, when writing,
190
+ if this is. This can also already be a file pointer like object
191
+ omitted then the keys of the first row dictionary passed to .add()
192
+ will be used for the fieldnames. If omitted when reading then the
193
+ first line of the csv file will be used for the fieldnames
194
+ :param encoding: string, what character encoing to use
195
+ :param **kwargs:
196
+ strict: bool, pass in True (default False) to have the class check
197
+ fieldnames when writing.
198
+ https://docs.python.org/3/library/csv.html#csv.Dialect.strict
199
+ extrasaction: str, "ignore" (default when strict is False) to
200
+ ignore extra fields, "raise" (default when strict is True) to
201
+ raise an error
202
+ write_mode: str, the default mode that will be passed to open a
203
+ stream for writing
204
+ readonly: bool, True if writing operations should fail
205
+ """
206
+ self.path = path
207
+ self.set_fieldnames(fieldnames)
208
+ self.writer = None
209
+ self.reader = None
210
+ self.strict = kwargs.pop("strict", False)
211
+
212
+ self.readonly = kwargs.pop("readonly", False)
213
+ if self.readonly:
214
+ self.write_mode = kwargs.pop("write_mode", "")
215
+ self.append_mode = kwargs.pop("append_mode", "")
216
+
217
+ else:
218
+ self.write_mode = kwargs.pop("write_mode", "w+")
219
+ self.append_mode = kwargs.pop("append_mode", "a+")
220
+
221
+ if not encoding:
222
+ encoding = environ.ENCODING
223
+ self.encoding = encoding
224
+
225
+ self.reader_row_class = kwargs.get("reader_row_class", CSVRow)
226
+ self.writer_row_class = kwargs.get("writer_row_class", None)
227
+ self.writer_class = kwargs.get("writer_class", csv.DictWriter)
228
+ self.reader_class = kwargs.get("reader_class", csv.reader)
229
+
230
+ def open(self, mode=""):
231
+ """Mainly an internal method used for opening the file pointers needed
232
+ for reading and writing
233
+
234
+ :param mode: string, the open mode
235
+ :returns: file pointer
236
+ """
237
+ if not mode or self.readonly:
238
+ mode = "r"
239
+
240
+ path = Filepath(self.path)
241
+
242
+ logger.debug("Opening csv file: {} using mode: {}".format(
243
+ path,
244
+ mode,
245
+ ))
246
+
247
+ return path.open(mode=mode, encoding=self.encoding)
248
+
249
+ @contextmanager
250
+ def reading(self):
251
+ """Internal method to manage reading operations
252
+
253
+ :returns: IOBase, the file pointer ready to be fully read
254
+ """
255
+ if isinstance(self.path, io.IOBase):
256
+ #logger.debug("Reading io: {}".format(self.path))
257
+ tell = self.path.tell()
258
+ self.path.seek(0)
259
+ try:
260
+ yield self.path
261
+
262
+ finally:
263
+ self.path.seek(tell)
264
+
265
+ else:
266
+ with self.open() as stream:
267
+ yield stream
268
+
269
+ @contextmanager
270
+ def writing(self):
271
+ """Internal method to manage writing operations
272
+
273
+ :returns: IOBase, the file pointer ready to be written to
274
+ """
275
+ if self.readonly:
276
+ raise IOError("CSV is in readonly mode")
277
+
278
+ if isinstance(self.path, io.IOBase):
279
+ #logger.debug("Writing io: {}".format(self.path.name))
280
+ yield self.path
281
+
282
+ else:
283
+ with self.open(self.write_mode) as stream:
284
+ yield stream
285
+
286
+ @contextmanager
287
+ def appending(self):
288
+ """The default context manager truncates and write a new file, but that
289
+ doesn't work for .add(), .append(), or .extend() so this provides an
290
+ alternative context manager for appending to the file
291
+ """
292
+ prev_mode = self.write_mode
293
+ self.write_mode = self.append_mode
294
+ try:
295
+ with self:
296
+ yield self
297
+
298
+ finally:
299
+ self.write_mode = prev_mode
300
+
301
+ def __enter__(self):
302
+ """Enables with context manager for writing"""
303
+ if not self.writer:
304
+ cm = self.writing()
305
+ self.writer = self.create_writer(cm.__enter__())
306
+ self.writer.cm = cm
307
+ # protection against multiple context managers
308
+ self.writer.context_depth = 0
309
+
310
+ self.writer.context_depth += 1
311
+ return self
312
+
313
+ def __exit__(self, exc_class, exc, trace):
314
+ self.writer.context_depth -= 1
315
+ if self.writer.context_depth <= 0:
316
+ self.writer.cm.__exit__(exc_class, exc, trace)
317
+ self.writer = None
318
+
319
+ def create_writer(self, stream, **kwargs):
320
+ kwargs.setdefault("dialect", csv.excel)
321
+ kwargs.setdefault("restval", "")
322
+ if self.strict:
323
+ kwargs.setdefault("strict", True)
324
+ kwargs.setdefault("extrasaction", "raise")
325
+ else:
326
+ kwargs.setdefault("strict", False)
327
+ kwargs.setdefault("extrasaction", "ignore")
328
+ kwargs.setdefault("quoting", csv.QUOTE_MINIMAL)
329
+ kwargs.setdefault("fieldnames", self.fieldnames)
330
+
331
+ writer = kwargs.pop("writer_class", self.writer_class)(
332
+ stream,
333
+ **kwargs
334
+ )
335
+ writer.has_header = True if stream.tell() > 0 else False
336
+ return writer
337
+
338
+ def create_reader(self, stream, **kwargs):
339
+ """create a csv reader, this exists to make it easy to customize
340
+ functionality, for example, you might have a csv file that doesn't have
341
+ column headers, so you can override this method to pass in the column
342
+ names, etc.
343
+
344
+ :param stream: IOBase, usually a file path opened with open()
345
+ :returns: csv.Reader instance or something that acts like a built-in
346
+ csv.Reader instance
347
+ """
348
+ kwargs.setdefault("dialect", csv.excel)
349
+ reader = kwargs.pop("reader_class", self.reader_class)(
350
+ stream,
351
+ **kwargs
352
+ )
353
+ return reader
354
+
355
+ def add(self, row):
356
+ """Add one row to the end of the CSV file
357
+
358
+ This is the set interface
359
+
360
+ :param row: dict, the row to be written
361
+ """
362
+ with self.appending():
363
+ writer = self.writer
364
+ if row := self.create_writer_row(row):
365
+ if not writer.has_header:
366
+ if not self.fieldnames:
367
+ self.set_fieldnames(row.keys())
368
+
369
+ writer.fieldnames = self.fieldnames
370
+ logger.debug("Writing fieldnames: {}".format(
371
+ ", ".join(self.fieldnames)
372
+ ))
373
+ writer.writeheader()
374
+ writer.has_header = True
375
+
376
+ writer.writerow(row)
377
+
378
+ def append(self, row):
379
+ """Add one row to the end of the CSV file
380
+
381
+ This is the list interface
382
+
383
+ :param row: dict, the row to be written
384
+ """
385
+ return self.add(row)
386
+
387
+ def extend(self, rows):
388
+ """Add all the rows to the end of the csv file
389
+
390
+ This is the list interface
391
+
392
+ :param rows: list, all the rows
393
+ """
394
+ with self.appending():
395
+ for row in rows:
396
+ self.add(row)
397
+
398
+ def update(self, rows):
399
+ """Add all the rows to the end of the csv file
400
+
401
+ This is the set and dict interface
402
+
403
+ :param rows: list, all the rows
404
+ """
405
+ return self.extend(rows)
406
+
407
+ def create_reader_row(self, columns, **kwargs):
408
+ """prepare row for reading, meant to be overridden in child classes if
409
+ needed"""
410
+ #return {k: columns[self.lookup[k]] for k in self.lookup}
411
+ return kwargs.get("reader_row_class", self.reader_row_class)(
412
+ columns,
413
+ self.lookup
414
+ )
415
+
416
+ def create_writer_row(self, row, **kwargs):
417
+ """prepare row for writing, meant to be overridden in child classes if
418
+ needed
419
+ """
420
+ writer_row_class = kwargs.get(
421
+ "writer_row_class",
422
+ self.writer_row_class
423
+ )
424
+ if writer_row_class:
425
+ row = writer_row_class(row, self.lookup)
426
+
427
+ else:
428
+ def get_value(v):
429
+ if self.strict:
430
+ return ByteString(v)
431
+
432
+ else:
433
+ # NOTE -- for some reason, the internal writer treats b""
434
+ # and ByteString(b"") differently, the b"" would be written
435
+ # out as "b""" which make me think it's doing repr(value)
436
+ # internally or something
437
+ return ByteString(b"" if v is None else v)
438
+
439
+ row = {
440
+ String(k): get_value(v) for k, v in row.items()
441
+ }
442
+
443
+ if self.strict:
444
+ rowcount = len(row)
445
+ fncount = len(self.fieldnames)
446
+ if rowcount != fncount:
447
+ raise ValueError(
448
+ "mismatch {} row(s) to {} fieldname(s)".format(
449
+ rowcount,
450
+ fncount
451
+ )
452
+ )
453
+
454
+ return row
455
+
456
+ def set_fieldnames(self, fieldnames):
457
+ """Set the fieldnames and create the lookup table
458
+
459
+ :param fieldnames: list[str], a list of fieldnames that corresponds
460
+ to the columns in the CSV file
461
+ """
462
+ if fieldnames:
463
+ self.fieldnames = list(map(String, fieldnames))
464
+ self.lookup = {item[1]: item[0] for item in enumerate(fieldnames)}
465
+
466
+ else:
467
+ self.fieldnames = []
468
+ self.lookup = {}
469
+
470
+ def find_fieldnames(self):
471
+ """attempt to get the field names from the first line in the csv file
472
+ """
473
+ if self.fieldnames:
474
+ return self.fieldnames
475
+
476
+ else:
477
+ for row in self:
478
+ return self.fieldnames
479
+
480
+ def rows(self):
481
+ """Return all the rows as a list"""
482
+ return self.tolist()
483
+
484
+ def tolist(self):
485
+ """Return all the rows as a list
486
+
487
+ Here for consistent interface as I'm using .tolist elsewhere
488
+
489
+ :returns: list
490
+ """
491
+ return list(self.__iter__())
492
+
493
+ def clear(self):
494
+ """clear the csv file"""
495
+ with self.writing() as stream:
496
+ stream.truncate(0)
497
+
498
+ def __iter__(self):
499
+ with self.reading() as stream:
500
+ ignore_row = True
501
+ reader = self.create_reader(stream)
502
+ for columns in reader:
503
+ if ignore_row:
504
+ if self.fieldnames:
505
+ # Check to see if the fieldnames are the first row of
506
+ # the CSV file, if they are then ignore this row
507
+ for i, col in enumerate(columns):
508
+ if self.fieldnames[i] != col:
509
+ ignore_row = False
510
+ break
511
+
512
+ else:
513
+ # if we don't have field names then the first row
514
+ # has to be the fieldnames
515
+ self.set_fieldnames(columns)
516
+
517
+ if not ignore_row:
518
+ ignore_row = False
519
+ row = self.create_reader_row(columns)
520
+ if row is not None:
521
+ yield row
522
+
523
+ def __len__(self):
524
+ """Returns how many rows are in this csv, this actually goes through
525
+ all the rows, so this is not a resource light method"""
526
+ count = 0
527
+ for r in self:
528
+ count += 1
529
+ return count
530
+
531
+ def __str__(self):
532
+ return self.read_text()
533
+
534
+ def read_text(self):
535
+ """This will print out all rows so it might not be ideal to use for
536
+ bigger CSVs"""
537
+ with self.reading() as stream:
538
+ return stream.read()
539
+
540
+
541
+ class TempCSV(CSV):
542
+ """Create a temporary CSV file in the system's tempdir"""
543
+ def __init__(self, fieldnames=None, **kwargs):
544
+ path = TempFilepath(kwargs.pop("path", ""), dir=kwargs.pop("dir", ""))
545
+ kwargs["fieldnames"] = fieldnames
546
+ super().__init__(path, **kwargs)
547
+