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/url.py ADDED
@@ -0,0 +1,988 @@
1
+ # -*- coding: utf-8 -*-
2
+ import re
3
+ import inspect
4
+ from socket import gethostname
5
+ import os
6
+
7
+ from .compat import *
8
+ from .string import String, ByteString
9
+ from .token import StopWordTokenizer
10
+
11
+
12
+ class Url(String):
13
+ r"""A url string on steroids, this is here to make it easy to manipulate
14
+ urls.
15
+
16
+ It tries to map the supported fields to their urlparse equivalents, with
17
+ some additions
18
+
19
+ https://en.wikipedia.org/wiki/URL
20
+ https://tools.ietf.org/html/rfc3986.html
21
+
22
+ A URI can be further classified as a locator, a name, or both. The
23
+ term "Uniform Resource Locator" (URL) refers to the subset of URIs
24
+ that, in addition to identifying a resource, provide a means of
25
+ locating the resource by describing its primary access mechanism
26
+ (e.g., its network "location").
27
+
28
+ foo://example.com:8042/over/there?name=ferret#nose
29
+ \_/ \______________/\_________/ \_________/ \__/
30
+ | | | | |
31
+ scheme authority path query fragment
32
+
33
+ Given a url:
34
+
35
+ http://user:pass@foo.com:1000/bar/che?baz=boom#anchor
36
+
37
+ .scheme = http
38
+ .netloc = user:pass@foo.com:1000
39
+ .authority = user:pass@foo.com:1000
40
+ .username = user
41
+ .password = pass
42
+ .userinfo = user:pass
43
+ .hostloc = foo.com:1000
44
+ .hostname = foo.com
45
+ .port = 1000
46
+ .path = bar/che
47
+ .fragment = anchor
48
+ .anchor = anchor
49
+ .uri = /bar/che?baz=boom#anchor
50
+ .host(...) = http://foo.com/...
51
+ .base(...) = http://user:pass@foo.com/bar/che/...
52
+ """
53
+ scheme = ""
54
+ """The http scheme (eg, "http" or "https")
55
+
56
+ By default this class won't set a scheme except when it can be "correctly"
57
+ inferred. But a child class could set the scheme to a default value and
58
+ then it will fallback to the set default
59
+
60
+ :example:
61
+ u = Url("//example.com")
62
+ u.scheme # "" because the // says the scheme wasn't wanted
63
+ u = Url("example.com:80")
64
+ u.scheme # "http" because a host was passed in with the plain port
65
+ """
66
+
67
+ username = None
68
+
69
+ password = None
70
+
71
+ hostname = ""
72
+
73
+ port = None
74
+
75
+ netloc = ""
76
+
77
+ path = ""
78
+
79
+ query_kwargs = {}
80
+
81
+ fragment = ""
82
+
83
+ @property
84
+ def root(self):
85
+ """just return scheme://netloc"""
86
+ if netloc := self.netloc:
87
+ ret = parse.urlunsplit((
88
+ self.scheme,
89
+ self.netloc,
90
+ "",
91
+ "",
92
+ ""
93
+ ))
94
+
95
+ else:
96
+ if self.scheme.startswith("http"):
97
+ ret = ""
98
+
99
+ else:
100
+ ret = self.scheme
101
+
102
+ return ret
103
+
104
+ @property
105
+ def anchor(self):
106
+ """alternative name for fragment"""
107
+ return self.fragment
108
+
109
+ @property
110
+ def authority(self):
111
+ """This is the official RFC name for netloc"""
112
+ return self.netloc
113
+
114
+ @property
115
+ def userinfo(self):
116
+ """The Wikipedia name for the user information before the @
117
+
118
+ https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax
119
+
120
+ :returns: str, a string containing <USERNAME>:<PASSWORD>
121
+ """
122
+ username = self.username or ""
123
+ password = self.password or ""
124
+ return f"{username}:{password}"
125
+
126
+ @property
127
+ def absolute(self):
128
+ """return the absolute URI, which is everything but base (no scheme,
129
+ host, etc). It's basically the path onwards
130
+ """
131
+ uristring = self.path
132
+ if self.query:
133
+ uristring += "?{}".format(self.query)
134
+ if self.fragment:
135
+ uristring += "#{}".format(self.fragment)
136
+
137
+ return uristring
138
+
139
+ @property
140
+ def paths(self):
141
+ """Returns the list of breadcrumbs for path, similar to path.Path.paths
142
+
143
+ Moved from bang.utils.Url on 1-6-2023
144
+
145
+ :returns: list, so if path was /foo/bar/che this would return
146
+ [/foo, /foo/bar, /foo/bar/che]
147
+ """
148
+ ret = []
149
+ paths = self.parts
150
+
151
+ for x in range(1, len(paths) + 1):
152
+ ret.append("/" + "/".join(paths[0:x]))
153
+
154
+ return ret
155
+
156
+ @property
157
+ def parts(self):
158
+ """Return a list of the path parts, similar to path.Path.parts"""
159
+ path = self.path.strip("/")
160
+ return path.split("/") if path else []
161
+
162
+ @property
163
+ def ext(self):
164
+ """return the extension of the file, the basename without the fileroot
165
+
166
+ Moved from bang.utils.Url.ext on 1-10-2023
167
+ """
168
+ return os.path.splitext(self.basename)[1].lstrip(".")
169
+
170
+ @property
171
+ def extension(self):
172
+ return self.ext
173
+
174
+ @property
175
+ def basename(self):
176
+ """Returns the basename of the path portion of the url
177
+
178
+ Moved from bang.utils.Url.ext on 1-10-2023
179
+ """
180
+ return os.path.basename(self.path)
181
+
182
+ @classmethod
183
+ def keys(cls):
184
+ # we need to ignore property objects also
185
+ def is_valid(k, v):
186
+ return not k.startswith("_") \
187
+ and not callable(v) \
188
+ and not isinstance(v, property) \
189
+ and not k.isupper()
190
+
191
+ keys = set(k for k, v in inspect.getmembers(cls) if is_valid(k, v))
192
+ return keys
193
+
194
+ @classmethod
195
+ def default_values(cls):
196
+ """Return the default values for the defined keys in the class
197
+
198
+ :returns: dict, the key is the default class variable name and the value
199
+ is the value it is set to
200
+ """
201
+ values = {}
202
+ for k in cls.keys():
203
+ v = getattr(cls, k)
204
+ if isinstance(v, dict):
205
+ values[k] = dict(v)
206
+
207
+ elif isinstance(v, list):
208
+ values[k] = list(v)
209
+
210
+ else:
211
+ values[k] = v
212
+
213
+ return values
214
+
215
+ @classmethod
216
+ def merge(cls, urlstring="", *args, **kwargs):
217
+ # we handle port before any other because the port of host:port in
218
+ # hostname takes precedence the port on the host would take precedence
219
+ # because proxies mean that the host can be something:10000 and the
220
+ # port could be 9000 because 10000 is being proxied to 9000 on the
221
+ # machine, but we want to automatically account for things like that
222
+ # and then if custom behavior is needed then this method can be
223
+ # overridden
224
+ parts = cls.default_values()
225
+
226
+ # we're going to remove our default scheme so we can make sure we set
227
+ # everything up correctly using passed in data, we'll add it back after
228
+ # we've got everything we needed
229
+ default_scheme = parts.pop("scheme", "")
230
+
231
+ if urlstring:
232
+ properties = [
233
+ "scheme", # 0
234
+ "netloc", # 1
235
+ "path", # 2
236
+ "query", # 3
237
+ "fragment", # 4
238
+ "username",
239
+ "password",
240
+ "hostname",
241
+ "port",
242
+ ]
243
+
244
+ has_host = is_url = False
245
+
246
+ if is_url := cls.is_url(urlstring):
247
+ o = parse.urlsplit(String(urlstring))
248
+
249
+ else:
250
+ s = String(urlstring)
251
+ part = s.split("/", maxsplit=1)[0]
252
+ has_host = "." in part or ":" in part \
253
+ or part.lower().startswith("localhost") \
254
+ or part.startswith("127.0.0.1")
255
+
256
+ if has_host:
257
+ # if we don't have a url but it looks like we have a host
258
+ # so let's make it a url by putting // in front of it so it
259
+ # will still parse correctly
260
+ s = "//{}".format(String(urlstring))
261
+
262
+ o = parse.urlsplit(s)
263
+
264
+ for k in properties:
265
+ v = getattr(o, k)
266
+ if v:
267
+ parts[k] = v
268
+
269
+ if is_url:
270
+ parts["scheme"] = getattr(o, "scheme")
271
+
272
+ query = parts.get("query", "")
273
+ if query:
274
+ parts["query_kwargs"].update(cls.parse_query(query))
275
+
276
+ query = kwargs.pop("query", "")
277
+ if query:
278
+ parts["query_kwargs"].update(cls.parse_query(query))
279
+
280
+ query_kwargs = kwargs.pop("query_kwargs", {})
281
+ if query_kwargs:
282
+ parts["query_kwargs"].update(query_kwargs)
283
+
284
+ parts["query"] = ""
285
+ if parts["query_kwargs"]:
286
+ parts["query"] = cls.unparse_query(parts["query_kwargs"])
287
+
288
+ ports = kwargs.pop("ports", {
289
+ "": 80,
290
+ "http": 80,
291
+ "https": 443,
292
+ })
293
+
294
+ for k, v in kwargs.items():
295
+ parts[k] = v
296
+
297
+ domain, port = cls.split_hostname_from_port(parts["hostname"])
298
+ parts["hostname"] = domain
299
+ if port:
300
+ parts["port"] = kwargs.get("port", port)
301
+
302
+ if not parts.get("port", None):
303
+ if "default_port" in kwargs:
304
+ parts["port"] = kwargs["default_port"]
305
+
306
+ else:
307
+ parts["port"] = ports.get(
308
+ parts.get("scheme", default_scheme),
309
+ None
310
+ )
311
+
312
+ # make sure port is an int
313
+ if parts["port"]:
314
+ parts["port"] = int(parts["port"])
315
+
316
+ hostloc = parts["hostname"]
317
+ port = parts["port"]
318
+ # we don't want common ports to be a part of a .geturl() call, but we
319
+ # do want .port to return them
320
+ if port and port not in set(ports.values()):
321
+ hostloc = '{}:{}'.format(hostloc, port)
322
+ parts["hostloc"] = hostloc
323
+
324
+ parts["netloc"] = parts["hostloc"]
325
+ username = kwargs.get("username", parts["username"])
326
+ password = kwargs.get("password", parts["password"])
327
+ if username:
328
+ parts["netloc"] = "{}:{}@{}".format(
329
+ username or "",
330
+ password or "",
331
+ parts["hostloc"]
332
+ )
333
+
334
+ parts["path"] = "/".join(cls.normalize_paths(parts["path"], *args))
335
+ # compensate for "/" being the first path part
336
+ if parts["path"].startswith("//"):
337
+ parts["path"] = parts["path"][1:]
338
+
339
+ if parts["path"]:
340
+ if parts["hostname"]:
341
+ # if path exists than we want to make sure it has a starting /
342
+ # if a hostname exists also, user could've passed in a relative
343
+ # path but we'll need to assume it is absolute since host
344
+ # exists
345
+ if not parts["path"].startswith("/"):
346
+ parts["path"] = "/" + parts["path"]
347
+
348
+ else:
349
+ if not parts.get("scheme", ""):
350
+ parts["scheme"] = ""
351
+
352
+ # let's add our default scheme now that we've generated everything
353
+ # we needed with the passed in values
354
+ if "scheme" not in parts:
355
+ if default_scheme:
356
+ parts.setdefault("scheme", default_scheme)
357
+
358
+ else:
359
+ if parts.get("hostloc", ""):
360
+ if parts.get("port", None) == 443:
361
+ parts.setdefault("scheme", "https")
362
+
363
+ else:
364
+ parts.setdefault("scheme", "http")
365
+
366
+ if not parts.get("scheme", ""):
367
+ parts["scheme"] = ""
368
+
369
+ parts["urlstring"] = parse.urlunsplit((
370
+ parts["scheme"],
371
+ parts["netloc"],
372
+ parts["path"],
373
+ parts["query"],
374
+ parts["fragment"],
375
+ ))
376
+
377
+ for k in parts:
378
+ if isinstance(parts[k], bytes):
379
+ parts[k] = String(parts[k])
380
+
381
+ return parts
382
+
383
+ @classmethod
384
+ def parse_query(cls, query):
385
+ """return name=val&name2=val2 strings into {name: val} dict"""
386
+ if not query:
387
+ return {}
388
+
389
+ if isinstance(query, bytes):
390
+ query = String(query)
391
+
392
+ try:
393
+ # https://docs.python.org/2/library/urlparse.html
394
+ query_kwargs = parse.parse_qs(query, True, strict_parsing=True)
395
+
396
+ except ValueError:
397
+ # try and parse the query manually, this will allow boolean values
398
+ # (arguments with no value)
399
+ query_kwargs = {}
400
+ for part in query.split("&"):
401
+ if "=" in part:
402
+ k, v = part.split("=", 1)
403
+ else:
404
+ k = part
405
+ v = "True"
406
+ query_kwargs.setdefault(k, [])
407
+ query_kwargs[k].append(v)
408
+
409
+ return cls.normalize_query_kwargs(query_kwargs)
410
+
411
+ @classmethod
412
+ def normalize_query_kwargs(cls, query_kwargs):
413
+ d = {}
414
+ # https://docs.python.org/2/library/urlparse.html
415
+ for k, kv in query_kwargs.items():
416
+ #k = k.rstrip("[]") # strip out php type array designated variables
417
+ if isinstance(k, bytes):
418
+ k = String(k)
419
+
420
+ if len(kv) > 1:
421
+ d[k] = kv
422
+
423
+ else:
424
+ d[k] = kv[0]
425
+
426
+ return d
427
+
428
+ @classmethod
429
+ def unparse_query(cls, query_kwargs):
430
+ """Given query kwargs in a dict format, convert them back into a string
431
+
432
+ This will encode [] in the name as discussed in:
433
+ https://stackoverflow.com/a/39294315/5006
434
+ https://github.com/ljharb/qs/issues/235
435
+
436
+ Luckily, it seems most servers will transparently handle encoded [] in
437
+ variable names
438
+
439
+ :param query_kwargs: dict, eg {"foo": "bar", "che": 1}
440
+ :returns: str, foo=bar&che=1
441
+ """
442
+ return urlencode(query_kwargs, doseq=True)
443
+
444
+ @classmethod
445
+ def normalize_paths(cls, *paths):
446
+ """turns a bunch of paths into something that can be concatenated
447
+ without any issues
448
+
449
+ :param *parts: str|list, things like "/foo/bar" or ["foo", "bar/che"]
450
+ :returns: list, a list of normalized parts with most of the "/" stripped
451
+ out, the exception is if the path is absolute then the first part
452
+ will be "/", so "/foo/bar" -> ["/", "foo", "bar"]
453
+ """
454
+ args = []
455
+ for ps in paths:
456
+ if not ps:
457
+ continue
458
+ if ps == ".":
459
+ continue
460
+
461
+ if isinstance(ps, int):
462
+ args.append(String(ps))
463
+
464
+ elif isinstance(ps, basestring):
465
+ if not args and ps.startswith("/"):
466
+ args.append("/")
467
+ ps.lstrip("/")
468
+
469
+ args.extend(filter(None, ps.split("/")))
470
+
471
+ else:
472
+ for p in ps:
473
+ nps = cls.normalize_paths(p)
474
+ if nps:
475
+ if args and nps[0] == "/":
476
+ args.extend(nps[1:])
477
+ else:
478
+ args.extend(nps)
479
+ return args
480
+
481
+ @classmethod
482
+ def split_hostname_from_port(cls, hostname, default_port=None):
483
+ """given a hostname:port return a tuple (hostname, port)"""
484
+ bits = hostname.split(":", 2)
485
+ p = int(default_port) if default_port else default_port
486
+ d = bits[0]
487
+ if len(bits) == 2:
488
+ p = int(bits[1])
489
+ return d, p
490
+
491
+ @classmethod
492
+ def is_full_url(cls, urlstring):
493
+ """Return True if urlstring is a url"""
494
+ return True if re.match(r"^\S+://", urlstring) else False
495
+
496
+ @classmethod
497
+ def is_relative_url(cls, urlstring):
498
+ """Return True if urlstring is a relative url (does not have a scheme
499
+ but instead starts with //)
500
+ """
501
+ return urlstring.startswith("//")
502
+
503
+ @classmethod
504
+ def is_path_url(cls, urlstring):
505
+ """Return true if this is a path url (url that doesn't have a host)
506
+
507
+ a path url: /foo/bar
508
+ """
509
+ return True if re.match(r"^/[^/]", urlstring) else False
510
+
511
+ @classmethod
512
+ def is_url(cls, urlstring):
513
+ """Returns True if urlstring is an actual full or relative url
514
+
515
+ Moved from bang.utils.Url.match on 1-10-2023
516
+ """
517
+ # REGEX = re.compile(r"^(?:https?:\/\/|\/\/)", re.I)
518
+ return cls.is_full_url(urlstring) or cls.is_relative_url(urlstring)
519
+
520
+ @classmethod
521
+ def create_instance(cls, *args, **kwargs):
522
+ #return type(self)(*args, **kwargs)
523
+ return Url(*args, **kwargs)
524
+
525
+ def __new__(cls, urlstring=None, *args, **kwargs):
526
+ """
527
+ :keyword hostname: Optional[str] the HOST:PORT or just HOST
528
+ """
529
+ parts = cls.merge(urlstring, *args, **kwargs)
530
+ urlstring = parts.pop("urlstring")
531
+ instance = super().__new__(cls, urlstring)
532
+ for k, v in parts.items():
533
+ setattr(instance, k, v)
534
+ return instance
535
+
536
+ def create(self, *args, **kwargs):
537
+ return self.create_instance(*args, **kwargs)
538
+
539
+ def add(self, **kwargs):
540
+ """Just a shortcut to change the current url, equivalent to
541
+ Url(self, **kwargs)
542
+ """
543
+ if "path" in kwargs:
544
+ path = kwargs["path"]
545
+ if isinstance(path, bytes):
546
+ path = String(path)
547
+ if not path[0].startswith("/"):
548
+ paths = self.normalize_paths(self.path, path)
549
+ else:
550
+ paths = self.normalize_paths(path)
551
+ kwargs["path"] = "/".join(paths)
552
+ return self.create(self, **kwargs)
553
+
554
+ def subtract(self, *paths, **kwargs):
555
+ sub_kwargs = self.jsonable()
556
+
557
+ path2 = self.normalize_paths(paths)
558
+ path2.extend(self.normalize_paths(kwargs.pop("path", "")))
559
+ if path2:
560
+ sub_path = self.normalize_paths(self.path)
561
+ for p in path2:
562
+ try:
563
+ sub_path.remove(p)
564
+ except ValueError:
565
+ pass
566
+
567
+ sub_kwargs["path"] = sub_path
568
+
569
+ for k, v in kwargs.items():
570
+ if k == "query_kwargs":
571
+ for qk, qv in kwargs[k].items():
572
+ if str(sub_kwargs[k][qk]) == str(qv):
573
+ sub_kwargs[k].pop(qk)
574
+
575
+ else:
576
+ if str(sub_kwargs[k]) == str(v):
577
+ sub_kwargs.pop(k)
578
+
579
+ return self.create(**sub_kwargs)
580
+
581
+ def _normalize_params(self, *paths, **query_kwargs):
582
+ """a lot of the helper methods are very similar, this handles their
583
+ arguments"""
584
+ kwargs = {}
585
+
586
+ if paths:
587
+ fragment = String(paths[-1])
588
+ if fragment:
589
+ if fragment.startswith("#"):
590
+ kwargs["fragment"] = fragment
591
+ paths.pop(-1)
592
+
593
+ kwargs["path"] = "/".join(self.normalize_paths(*paths))
594
+
595
+ kwargs["query_kwargs"] = query_kwargs
596
+ return kwargs
597
+
598
+ def parent(self, *paths, **query_kwargs):
599
+ """create a new Url instance one level up from the current Url instance
600
+
601
+ so if self contains /foo/bar then self.parent() would return /foo
602
+
603
+ :param *paths: list, the paths to append to the parent path
604
+ :param **query_kwargs: dict, any query string params to add
605
+ :returns: new Url instance
606
+ """
607
+ kwargs = self._normalize_params(*paths, **query_kwargs)
608
+ path_args = self.path.split("/")
609
+ if path_args:
610
+ urlstring = self.subtract(path_args[-1])
611
+ else:
612
+ urlstring = self
613
+
614
+ return urlstring.add(**kwargs)
615
+
616
+ def base(self, *paths, **query_kwargs):
617
+ """create a new url object using the current base path as a base
618
+
619
+ if you had requested /foo/bar, then this would append *paths and
620
+ **query_kwargs to /foo/bar
621
+
622
+ :example:
623
+ # current path: /foo/bar
624
+
625
+ print url # http://host.com/foo/bar
626
+
627
+ print url.base() # http://host.com/foo/bar
628
+ print url.base("che", boom="bam") # http://host/foo/bar/che?boom=bam
629
+
630
+ :param *paths: list, the paths to append to the current path without
631
+ query params
632
+ :param **query_kwargs: dict, any query string params to add
633
+ """
634
+ kwargs = self._normalize_params(*paths, **query_kwargs)
635
+ if self.path:
636
+ if "path" in kwargs:
637
+ paths = self.normalize_paths(self.path, kwargs["path"])
638
+ kwargs["path"] = "/".join(paths)
639
+ else:
640
+ kwargs["path"] = self.path
641
+ return self.create(self.root, **kwargs)
642
+
643
+ def host(self, *paths, **query_kwargs):
644
+ """create a new url object using the host as a base
645
+
646
+ if you had requested http://host/foo/bar, then this would append *paths
647
+ and **query_kwargs to http://host
648
+
649
+ :example:
650
+ # current url: http://host/foo/bar
651
+
652
+ print(url) # http://host.com/foo/bar
653
+
654
+ print(url.host()) # http://host.com/
655
+ print(url.host("che", boom="bam")) # http://host/che?boom=bam
656
+
657
+ :param *paths: list, the path parts to append to the current host
658
+ :param **query_kwargs: dict, any query string params to add
659
+ """
660
+ kwargs = self._normalize_params(*paths, **query_kwargs)
661
+
662
+ # because path is from host then it is automatically absolute
663
+ if "path" in kwargs and not kwargs["path"].startswith("/"):
664
+ kwargs["path"] = "/" + kwargs["path"]
665
+
666
+ return self.create(self.root, **kwargs)
667
+
668
+ def child(self, *paths, **kwargs):
669
+ """Layers paths and kwargs on top of self, using passed in values to
670
+ replace anything in self
671
+
672
+ :Example:
673
+ u = Url("foo", 1)
674
+ print(u) # foo/1
675
+
676
+ u2 = u.child("bar", 2)
677
+ print(u2) # foo/1/bar/2
678
+
679
+ u3 = u.child(path=["bar", 2])
680
+ print(u2) # bar/2
681
+
682
+ :param *paths: url path parts
683
+ :param **kwargs: same kwargs that can be passed into creation
684
+ :returns: a new instance of this class
685
+ """
686
+ return self.create(self, *paths, **kwargs)
687
+
688
+ def jsonable(self):
689
+ return dict(
690
+ scheme=self.scheme,
691
+ username=self.username,
692
+ password=self.password,
693
+ hostname=self.hostname,
694
+ port=self.port,
695
+ path=self.path,
696
+ query_kwargs=self.query_kwargs,
697
+ fragment=self.fragment,
698
+ )
699
+
700
+ def copy(self):
701
+ return self.__deepcopy__()
702
+
703
+ def __copy__(self):
704
+ return self.__deepcopy__()
705
+
706
+ def __deepcopy__(self, memodict={}):
707
+ return self.create(**self.jsonable())
708
+ # scheme=self.scheme,
709
+ # username=self.username,
710
+ # password=self.password,
711
+ # hostname=self.hostname,
712
+ # port=self.port,
713
+ # path=self.path,
714
+ # query_kwargs=self.query_kwargs,
715
+ # fragment=self.fragment,
716
+ # )
717
+
718
+ def __add__(self, other):
719
+ ret = ""
720
+ if isinstance(other, Mapping):
721
+ ret = self.add(query_kwargs=other)
722
+
723
+ elif isinstance(other, MutableSequence):
724
+ ret = self.add(path=other)
725
+
726
+ elif isinstance(other, basestring):
727
+ ret = self.add(path=other)
728
+
729
+ elif isinstance(other, Sequence):
730
+ ret = self.add(path=other)
731
+
732
+ else:
733
+ raise ValueError("Not sure how to add {}".format(type(other)))
734
+
735
+ return ret
736
+ __iadd__ = __add__
737
+
738
+ def __truediv__(self, other):
739
+ ret = ""
740
+ if isinstance(other, MutableSequence):
741
+ ret = self.add(path=other)
742
+
743
+ elif isinstance(other, basestring):
744
+ ret = self.add(path=other)
745
+
746
+ elif isinstance(other, Sequence):
747
+ ret = self.add(path=other)
748
+
749
+ else:
750
+ raise ValueError("Not sure how to add {}".format(type(other)))
751
+
752
+ return ret
753
+ __itruediv__ = __truediv__
754
+
755
+ def __sub__(self, other):
756
+ """Return a new url with other removed"""
757
+ ret = ""
758
+ if isinstance(other, Mapping):
759
+ ret = self.subtract(query_kwargs=other)
760
+
761
+ elif isinstance(other, MutableSequence):
762
+ ret = self.subtract(path=other)
763
+
764
+ elif isinstance(other, basestring):
765
+ ret = self.subtract(path=other)
766
+
767
+ elif isinstance(other, Sequence):
768
+ ret = self.subtract(path=other)
769
+
770
+ else:
771
+ raise ValueError("Not sure how to add {}".format(type(other)))
772
+
773
+ return ret
774
+ __isub__ = __sub__
775
+
776
+ def jsonable(self):
777
+ ret = {}
778
+ for k in self.keys():
779
+ v = getattr(self, k)
780
+ if k == "query_kwargs":
781
+ ret[k] = dict(v)
782
+ else:
783
+ ret[k] = v
784
+
785
+ return ret
786
+
787
+ def has_hostname(self):
788
+ """Return True if this url has a host"""
789
+ return True if self.hostname else False
790
+
791
+ def is_hostname(self, hostname):
792
+ """return true if the url's host matches host"""
793
+ ret = False
794
+ if not self.has_hostname() and not hostname:
795
+ ret = True
796
+ else:
797
+ ret = self.hostname.lower() == hostname.lower()
798
+ return ret
799
+
800
+ def is_relative(self):
801
+ """return True if this url is a path or relative url
802
+
803
+ a relative url: //hostname.tld/foo/bar
804
+ """
805
+ return self.is_relative_url(self)
806
+
807
+ def is_full(self):
808
+ """return True if this url is a path or relative url
809
+
810
+ a full url: scheme//hostname.tld/foo/bar
811
+ """
812
+ return self.is_full_url(self)
813
+
814
+ def is_path(self):
815
+ """Return true if this is a path url (url that doesn't have a host)
816
+
817
+ a path url: /foo/bar
818
+ """
819
+ return self.is_path_url(self)
820
+
821
+ def is_local(self):
822
+ """return True if is a localhost url"""
823
+ return (
824
+ self.is_path()
825
+ or self.is_hostname("localhost")
826
+ or self.is_hostname("127.0.0.1")
827
+ )
828
+
829
+ def unsplit(self):
830
+ """By default, the URL won't contain a scheme if it wasn't passed in,
831
+ this will add the default scheme and return the url"""
832
+ return parse.urlunsplit((
833
+ self.scheme,
834
+ self.netloc,
835
+ self.path,
836
+ self.query,
837
+ self.fragment,
838
+ ))
839
+
840
+ def full(self):
841
+ """alias of .unsplit()"""
842
+ return self.unsplit()
843
+ Urlstring = Url
844
+ UrlString = Url
845
+
846
+
847
+ class Host(tuple):
848
+ """Creates a tuple (hostname, port) that can be passed to built-in Python
849
+ server classes and anything that uses that same interface, does all the
850
+ lifting to figure out what the hostname and port should be based on the
851
+ passed in input, so you can pass things like hostname:port, or (host,
852
+ port), etc.
853
+
854
+ https://docs.python.org/3/library/socket.html#module-socket - creates an
855
+ AF_INET server address
856
+ """
857
+ @property
858
+ def hostname(self):
859
+ """Return just the hostname with no port or scheme or anything
860
+
861
+ :returns: string
862
+ """
863
+ return self[0]
864
+
865
+ @property
866
+ def port(self):
867
+ """return just the port
868
+
869
+ :returns: int
870
+ """
871
+ return self[1]
872
+
873
+ @property
874
+ def hostloc(self):
875
+ """Returns hostname:port
876
+
877
+ :returns: string
878
+ """
879
+ return self.url.hostloc
880
+
881
+ def __new__(cls, host="", port=None):
882
+ """Create a new server_address tuple
883
+
884
+ If nothing is passed in the default address is ("", 0) which in most
885
+ python stdlib server code will bind to localhost with some random
886
+ free port
887
+
888
+ :param host: str|tuple[str, int|str], can handle things strings like
889
+ ":<PORT>", "<HOSTNAME>:<PORT>", "<HOSTNAME>", or
890
+ ["<HOSTNAME>", "<PORT>"]
891
+ :param port: int
892
+ """
893
+ if isinstance(host, tuple):
894
+ # compensate for passing in a server_address tuple which I seem
895
+ # to do a lot evidently
896
+ port = host[1]
897
+ host = host[0]
898
+
899
+ if host.startswith(":"):
900
+ # host is just a port like ":8000"
901
+ port = host[1:]
902
+ host = ""
903
+
904
+ u = Url(host, default_port=port)
905
+
906
+ port = u.port
907
+ if not port:
908
+ # If the port is empty we set it to 0 so it can trigger python's
909
+ # internal server code to find an available port. If we didn't do
910
+ # this we would get the error: TypeError: 'NoneType' object cannot
911
+ # be interpreted as an integer
912
+ port = 0
913
+
914
+ instance = super().__new__(cls, [u.hostname, port])
915
+ instance.url = u
916
+ return instance
917
+
918
+ def __str__(self):
919
+ return self.hostloc
920
+
921
+ def __bytes__(self):
922
+ return ByteString(self.hostloc)
923
+
924
+ def full(self):
925
+ return self.url.full()
926
+
927
+ def client(self):
928
+ """Url can technically hold a hostname like 0.0.0.0, this will
929
+ compensate for that, useful for test clients
930
+
931
+ :returns: str, a netloc (eg "host:port") that a client can use to make
932
+ a request
933
+ """
934
+ domain, port = self
935
+
936
+ netloc = domain
937
+
938
+ if domain == "0.0.0.0":
939
+ netloc = gethostname()
940
+
941
+ elif not domain:
942
+ netloc = "127.0.0.1"
943
+
944
+ if port:
945
+ netloc += ":{}".format(port)
946
+
947
+ return netloc
948
+
949
+
950
+ class SlugWords(StopWordTokenizer):
951
+ """Internal class used by Slug to find all the valid slug words"""
952
+
953
+ NON_ASCII_REGEX = re.compile(r'[^\x00-\x7F]+')
954
+ """non-ascii characters
955
+
956
+ https://stackoverflow.com/a/2759009/5006
957
+ """
958
+
959
+ def normalize(self, token):
960
+ word = token.text
961
+ word = self.NON_ASCII_REGEX.sub("", word)
962
+ return word.lower()
963
+
964
+ def is_valid(self, word):
965
+ return word and (word not in self.STOP_WORDS)
966
+
967
+
968
+ class Slug(String):
969
+ """Given a string, convert it into a slug that can be used in a url path
970
+
971
+ This tokenizes the passed in string, strips non-ascii, punctuation, and
972
+ stop words and then returns the remaining words joined by delim (default is
973
+ a hyphen)
974
+ """
975
+ def __new__(cls, slug, **kwargs):
976
+ """
977
+ :param val: str, the prospective slug
978
+ :keyword delim: str, default is a hyphen
979
+ :keyword size: int, the max size you want the slug
980
+ """
981
+ delim = kwargs.get("delim", "-")
982
+ words = SlugWords(slug)
983
+ slug = delim.join(words)
984
+ if size := kwargs.get("size", 0):
985
+ slug = String(slug).truncate(size, sep=delim)
986
+
987
+ return super().__new__(cls, slug, **kwargs)
988
+