Module xurls.url
Helper classes to work with Url's.
Allows you to easily create, work with and append Urls together.
Main classes:
- Url
Path Formatting Placeholders:
The path
can contain template formatting placeholders, just like you would do in a
normal python string. These ones MUST be named, however, and not positional formatting
placeholders.
At some point in the future, I will probably support formatting placeholders for a query value. For now, it's only for the path.
Here is an example:
"http://api.com/v1/accounts/{id}"
If later on, we add a query like this to the Url:
Url.query_add()
('id', 1)
And then call url.url()
, we will get back this:
"http://api.com/v1/accounts/1"
You can also pass in a secondary/backup list of key/values into the .url(…) method, like so:
>>> url = Url("https://api.com/some/endpoint/{id}")
>>> url.url(secondary_values={'id': 9})
"https://api.com/some/endpoint/9"
It can also be an object, which in this case it will look for an attribute of id
.
Url.url()
will return None
if it can't construct the Url due to an absent format
value that it can't find. In this way, you could have a list of Url's and the first
one that can be formatted is the one you use.
You can also use Url.is_valid()
to see if the url is valid in a more efficent way,
as it won't need to format the full url to determine this.
Value for formatting placeholders come from these sources:
- First look in Url's query values (
Url.query
), looking for a value with same key-name. - We next look in any secondary_values passed into the
Url.url()
method for the value we need if not found in query-value. - Finally, we will fallback attached values, you can attach values to a mutable url
via
Url.is_valid()
; see doc-comment in that method for more details.
TODO
At some point in the future I'll probably support place-holders for
query-key/values.
The format_placeholders
argument would also control
this too once we get around to supporting that.
Expand source code
"""
Helper classes to work with Url's.
Allows you to easily create, work with and append Urls together.
Main classes:
- `Url`
## Path Formatting Placeholders:
[path-formatting-placeholders]: #path-formatting-placeholders
The `path` can contain template formatting placeholders, just like you would do in a
normal python string. These ones MUST be named, however, and not positional formatting
placeholders.
At some point in the future, I will probably support formatting placeholders for
a query value. For now, it's only for the path.
Here is an example:
`"http://api.com/v1/accounts/{id}"`
If later on, we add a query like this to the Url:
`Url.query_add`('id', 1)
And then call `url.url()`, we will get back this:
`"http://api.com/v1/accounts/1"`
You can also pass in a secondary/backup list of key/values into the .url(...) method,
like so:
>>> url = Url("https://api.com/some/endpoint/{id}")
>>> url.url(secondary_values={'id': 9})
"https://api.com/some/endpoint/9"
It can also be an object, which in this case it will look for an attribute of `id`.
`Url.url` will return `None` if it can't construct the Url due to an absent format
value that it can't find. In this way, you could have a list of Url's and the first
one that can be formatted is the one you use.
You can also use `Url.is_valid` to see if the url is valid in a more efficent way,
as it won't need to format the full url to determine this.
Value for formatting placeholders come from these sources:
1. First look in Url's query values (`Url.query`), looking for a value with same key-name.
3. We next look in any secondary_values passed into the `Url.url` method for the value
we need if not found in query-value.
2. Finally, we will fallback attached values, you can attach values to a mutable url
via `Url.is_valid`; see doc-comment in that method for more details.
.. todo:: At some point in the future I'll probably support place-holders for
query-key/values. The `format_placeholders` argument would also control
this too once we get around to supporting that.
"""
from __future__ import annotations
from typing import (
Type,
Optional,
Union,
TypeVar,
Dict,
Iterable,
get_type_hints,
List,
Set,
Sequence,
Any,
Iterator,
)
import datetime as dt
from copy import copy
from urllib import parse as urlparser
from types import MappingProxyType
from dataclasses import dataclass
import string
from warnings import warn
from xloop import xloop
# I'm not going to worry about url 'parameters' for now,
# Url parameter example: "http://www.google.com/some_path;paramA=value1;paramB=value2"
from xsentinels import Default
T = TypeVar("T")
SecondaryValues = Union[object, dict, Sequence[object]]
class _FormattedQueryValue:
"""
Formats query values into the final formatted string.
`_FormattedQueryValue.url_query` is what `Url` calls when it needs to format it's self
into a string via `Url.url`. It will be called on the first item (or only item) if it's
a _FormattedQueryValue type value. If it's not, it will use a plain/default instance of
`_FormattedQueryValue`.
By default `_FormattedQueryValue.url_query` will call `_FormattedQueryValue.url_query_value`
for each value encountered (and if the original value was a list, for each item in the list).
It then combines these together into a single string by a the
`UrlFormattingOptions.list_value_delimiter` and if there is more than one value
adds a `UrlFormattingOptions.list_key_suffix` to the end of key if the key does not
already have that suffix.
.. note:: Thinking about putting in an ability to change the default formatter via a
`xinject.context.Resource`. That way it could easily be overridden via a
`xinject.context.Context`. I'll do that if/when we need it.
For now the default _FormattedQueryValue is just a plain instance that is shared
via a private module attribute.
"""
def url_query(
self, *, key: str, url: Url, url_options: UrlFormattingOptions, values: List[Any]
) -> Query:
"""
If `_FormattedQueryValue` encountered as a query value as the first or only value while
trying to convert a Url to a string we will call `url_query` on that value.
If the first item is not a `_FormattedQueryValue` (such as a plain `str`) then and instance
of this class (not a sub-class) will be directly used for format the query value into
what is needed in final formatted Url by returning a dict of the needed key/str-values
to append to final formatted Url.
It's expected this will return a dict with whatever key/values are needed to
represent the value in a url query.
By default (unless method overriden via a sub-class) this method will go though each
value passed in under `values` paramter and call `_FormattedQueryValue.url_query_value`
on each one. If there is only one value, this still still happen but just for that single
value. Method `_FormattedQueryValue.url_query_value` is expected to convert this value
into a string. It's possible to have a subclass in simple cases to only just override
`_FormattedQueryValue.url_query_value` and let us call that for each value. We would
then combine the strings produced into a single string (like you would expect).
Args:
key (str): Key that is currently being used for the query name for our value (self).
url (Url): Url that is being formatted, so you can examine it if needed
url_options (UrlFormattingOptions): Options that are being used to format the url.
values (List[Any]): A list of the query values as orginally set into the Url;
this means the values can be anything.
Returns:
A dictionary with whatever key/values you need. Keep in mind that if the key
name(s) returned conflict with other keys, only one will be selected and the others
ignored, depending on the order the original options were defined in. Values with
new keys added to the original Url's query later will take priority over earlier ones.
Example:
>>> class MyValueType(_FormattedQueryValue):
... def url_query(self, *args, **kwargs):
... return {'key-1': 'hello!'}
>>>
>>> my_url = Url()
>>> my_url.query_add('key-1', "my-value")
>>> my_url.query_add('key-2', MyValueType())
In this example, type `MyValueType` implements _FormattedQueryValue and returns
>>> {'key-1': 'hello!'}
Since it's defined after adding `my-value` it will overwrite it with
`hello!`
>>> my_url.url()
"?key-1=hello"
"""
delimiter = url_options.list_value_delimiter
suffix = url_options.list_key_suffix
keep_suffixes = url_options.list_key_suffixes_to_keep
final_items = []
for item in values:
call_on = self
if isinstance(item, _FormattedQueryValue):
call_on = item
str_val = call_on.url_query_value(url=url, url_options=url_options, value=item)
if not str_val:
continue
final_items.append(str_val)
if len(final_items) == 1:
new_val = final_items[0]
else:
# If we already end with suffix, don't add it.
if not key.endswith(suffix):
add_suffix = True
for s in keep_suffixes:
if key.endswith(s):
add_suffix = False
break
if add_suffix:
key = key + suffix
# todo: consider raising an exception if we have blank/no delimiter.
if delimiter:
new_val = delimiter.join(final_items)
else:
# We send back a list if there is no delimiter, it will format url like this
# with duplicate keys in the string:
# '?key:final_items-1&key:final_items-2&....'
new_val = final_items
return {key: new_val}
def url_query_value(self, *, url: Url, url_options: UrlFormattingOptions, value: Any) -> str:
"""
`_FormattedQueryValue.url_query` calls this method is call for each value in the query.
If a query item has a list with more than one values this is normally called on each
value in that list and then combined together via url formatting options.
Whatever value is returned will be incorporated into the query via the
UrlFormattingOptions: `list_key_suffix` and `list_value_delimiter` attributes inside
`_FormattedQueryValue.url_query`.
Args:
url: Url that is being formatted, so you can examine it if needed
url_options: Options that are being used to format the url.
value: The specific value to format. if this value is a _FormattedQueryValue then
it's guaranteed that `self` will be the same as the passed in value.
If some values inside query-value of url are a `_FormattedQueryValue` and
others are not then it's possible for this method to be called where the
`value` is that other value and self will be the first _FormattedQueryValue
in the query list. If the first value in the query list is something else
then the default _FormattedQueryValue is used (just a plain instance),
and that's what `self` will be.
"""
if isinstance(value, dt.date):
return value.isoformat()
elif value or isinstance(value, int):
return str(value)
_default_query_formatter = _FormattedQueryValue()
""" Class that's used to format query values when the first/only value is a plain str (string). """
QueryValue = Union[
str,
int,
dt.date,
_FormattedQueryValue,
None,
Iterable[Union[str, int, dt.date, _FormattedQueryValue]],
]
""" A query value can be either a str, or a list of str.
If it is a list of str, then the key will have __in appended and the values will be delimited
by a comma ',' if Url.use_in_operator_for_query_lists is True, ie: `?someKey__in=A,B`.
Otherwise they will be duplicated, ie: `?someKey=A&someKey=B`.
If Url.use_in_operator_for_query_lists is False, we instead duplicate the key
for each value in the list in final url, ie: (1, 2) turns into: 'a_key=1&a_key=2'.
use_in_operator_for_query_lists is True by default.
"""
Query = Dict[str, QueryValue]
""" Type that represents the entire query portion of a Url.
"""
@dataclass
class UrlFormattingOptions:
# TODO: Allow for a configurable formatting callback function/object.
list_key_suffix: str = '__in'
""" What to strip/append to query key-name if the value is a list.
When parsing a Url, if the key name does not end with this, we won't try to split the value
by the `list_value_delimiter`, the value will be left unchanged.
When parsing a Url, the list_key_suffix will be striped from the key-name. When
constructing the Url into a string, list_key_suffix will be added to a key-name if the
value is a list.
Default is '__in', which is the standard way Django Rest Framework's filters work.
"""
list_key_suffixes_to_keep = [
'__fields__include',
'__fields__only',
'__fields__exclude'
]
""" Suffixes listed here can implicitly take a delimited list of values
without using the generalized `UrlFormattingOptions.list_key_suffix` (see above).
The `UrlFormattingOptions.list_key_suffix` won't be used if a key ends with one of
these keys, but instead the list will be directly formatted/parsed directly out of
the value without modifying the key.
"""
list_value_delimiter: str = ','
""" This tells the Url the character to split query value by.
If there is no delimiter in value, then a list with a single value is the result.
Keep in mind that list won't be parsed from a queries value if the queries key-name
does not end with `list_key_suffix`. The `list_key_suffix` is what controls if the
query value is a list or not.
Default is a comma `,`.
"""
DefaultQueryValueListFormat = UrlFormattingOptions()
""" The default if `formatting_options` is None.
Produces a format that does this when you have a query-key with multiple values:
"http://host/path?key-name__in=value1,value2"
When you do this:
`query_add('key-name', ['value1', 'value2'])`
"""
DuplicateKeyNamesQueryValueListFormat = UrlFormattingOptions(
list_key_suffix='', list_value_delimiter=''
)
""" Produces a format that does this when you have a query-key with multiple values:
"http://host/path?key-name=value1&key-name=value2"
When you do this:
`query_add('key-name', ['value1', 'value2']`
"""
HTTPMethodType = str
# Standard Methods
HTTPGet = 'GET'
HTTPPatch = 'PATCH'
""" Represents http PATCH method. """
HTTPDelete = 'DELETE'
HTTPPost = 'POST'
HTTPPut = 'PUT'
class Url:
"""
Allows you to easily create, work with and append Urls together.
You can pass a normal Url or string into the first argument of `Url.__init__`, and if you do
it as a string it will be parsed as a Url and it's components will be set into the
returned as a Url with it's various components parsed into Url's various attributes.
There is also a parameter for each Url component attribute in the `Url.__init__` method.
If no value is provided for a particular url component, it will be assigned a `None` value.
Use `Url.url()` method to get a url `str` back from components.
For more details on path features,
see [Path Formatting Placeholders](#path-formatting-placeholders)
## Mutating Methods
A big way to use Url is to `Url.append_url` `Url`'s together. You can then take
different `Url`'s that have different components and construct a final `Url` to use.
When appending a Url to another [via `Url.append_url`], if any particular component
has a None value, it will not change the Url it's being appended to for that particular
component. This allows you to append Urls together and only relevant/set-values will
be whats appended. This allows you to construct Urls based on pieces of Urls to construct
the final Url.
When you append a Url, the path and query and methods are treated in a special way:
- `Url.path`: it will be appended to the destination Url via
`Url.append_path`, and therefore, the path will be appended onto the existing
path (with a `/` if needed to separate them).
- `Url.query`: Keys will be added via `Url.query_add`, and therefore the only
keys that will be replaced are ones where the same key-name exists in both Urls.
Otherwise, the keys are 'added' together.
- `Url.methods`: This will get union'd with the existing `methods` in the destination
url.
The methods that mutate self [such as 'append_path', etc] return self, so you can chain
them together with other mutating methods.
Example Usages:
>>> urlc = Url()
>>> urlc.scheme = 'http'
>>> urlc.host = 'www.google.com'
>>> urlc.url()
"http://www.google.com"
>>> urlc = Url("http://www.google.com").append_url("/hello")
>>> urlc.url()
"http://www.google.com/hello"
"""
@classmethod
def ensure_url(
cls: Type[T],
value: Optional[UrlStr],
*,
default_formatting_options: UrlFormattingOptions = Default,
) -> T:
"""Will check to see if the passed in value is a `Url` (depending on the
class you call `Url.ensure_url` on). If so, will return value unchanged.
This is an optimization and the reason to use this method over just constructing
a new `Url` and passing the value into that. If the value is already a Url
and it's formatting_options don't need to be set to `default_formatting_options`
it will just return it without any extra work.
If not then will create a new instance of cls and pass value to `__init__`
and return result.
### If you provide `default_formatting_options`:
If you pass in a `str` to this method, then any `formatting_options` you passed in
will be checked and used if provided.
If you pass in a Url, and it has not explicitly set formatting_options, we
will make a copy of passed in Url and set `default_formatting_options` on it
and return copy.
Otherwise, Url will use the usual `DefaultQueryValueListFormat` for the default format.
Args:
value: You can pass in a UrlStr (which is either a `str` or `Url`), and you will get
back a `Url` object from it. If the value is exactly the same as the class you call
this class method on, you will get it back unchanged. If not then we create a new
object of type `cls` and give value to it as a init parameter.
If `value` is not a `Url` or `str`, we will try and convert it to a `str` for
you in an attempt to extract a url string from the passed in object.
default_formatting_options (UrlFormattingOptions): Formatting options to use IF
passed in value is a string. If the passed in value is a Url object, we use
whatever it has set on it. If no formatting options are set on the `Url` object
that is passed in, and you pass in a default_formatting_options, we will copy
passed in Url and set that formatting option on it, and return copy.
Returns:
Url: Object of same type as `cls`, which is the class you called `Url.ensure_url` on.
"""
if type(value) is cls and (not default_formatting_options or value.formatting_options):
return value
if isinstance(value, Url) and value.formatting_options:
# Url already has explicitly set formatting-options, keep whatever it has.
return cls(value)
# Otherwise, we are some other non-Url value (ie: a `str` probably),
# try tp convert value to string and any default_formatting_options the user provided.
return cls(value, formatting_options=default_formatting_options)
def __init__(
self,
url: Union[str, Url] = None,
*,
scheme: str = Default,
username: str = Default,
password: str = Default,
host: str = Default,
port: int = Default,
path: str = Default,
query: Query = Default,
fragment: str = Default,
formatting_options: UrlFormattingOptions = Default,
# Params below are extra metadata about Url that is useful to communicate about:
# TODO: Find a more generalized way to include 'mergeable'/'appendable' metadata
# in the Url object; For now keeping them, but need to remove in the
# next major release.
singular: bool = None,
methods: Union[str, Iterable[str]] = None,
):
"""
# `__init__` Specifics
Pass in a single 'url' or pass in the individual components.
If you pass in both, the individual components [kw-args] will take precedence.
If you pass in a query parameter arg, it will completely replace entire query from
any that were provided in `url` string. Same goes with the 'path'.
Use 'append_url(...)' to easily merge/append query/paths together.
If `xsentinels.Default` is left in place, then that value will be ignored and not replace
anything that was parsed for that component from `url` string. Unless the 'url'
pass in defined that particular component it will be set to None. When you append
a Url on another with values set to None, it will not 'append' them into the Url.
If you pass in a None, we will replace that particular value with a None inside the
Url.
All parameters are optional. Providing no parameters at all creates a completely
blank Url. If you ask for the .url() of a blank Url, an empty string is returned.
Here is an example of all of these basic Url components in their proper place:
`scheme://username:password@host:port/path?query#fragment`
Args:
url (str, Url): A normal Url formatted string or Url object;
ie: "www.google.com/hello".
If provided, forms the basis of the Url.
All other parameters will over-write that particular
component of the url provided here
[ie: the other parameters take priority].
scheme: Replaces/Sets scheme on new Url object.
password: Replaces/Sets password on new Url object.
host: Replaces/Sets host on new Url object.
port: Replaces/Sets port on new Url object.
path: Replaces/Sets path on new Url object.
Path's can have variable placeholders in them, see
[Path Formatting Placeholders](#path-formatting-placeholders).
fragment: Replaces/Sets fragment on new Url object.
query: Replaces/Sets query on new Url object.
formatting_options:
Way to configure how a query value can be split up into a
list when parsing a url, and back again when reconstructing the url into a string.
For details, see the `UrlFormattingOptions` class.
If set with None, `Url` class uses `DefaultQueryValueListFormat` by default
for it's formatting options.
singular: (deprecated: need more generalized way to include metadata)
Provides a hint to indicate if the body of the request/response should be a
list or a single object. If 'None' [default], then the underlying system will do
it's best to guess based on other factors.
methods: (deprecated: need more generalized way to include metadata)
If useful, you can provide a hint of which HTTP methods this Url is
valid for. Defaults to an empty set. When you append methods to another Url,
the result is a union of both sets of methods from the two Url's.
"""
if formatting_options is not Default:
self._formatting_options = formatting_options
if methods is not None:
warn(
"Url.methods deprecated pending a more generalized way to include metadata.",
DeprecationWarning, 2
)
self._set_methods(methods)
else:
self._methods = set()
if singular not in (None, Default):
warn(
"Url.singular deprecated pending a more generalized way to include metadata.",
DeprecationWarning, 2
)
self._singular = singular
if isinstance(url, str):
result: urlparser.ParseResult = urlparser.urlparse(url)
self._scheme = result.scheme or None
value = result.username
self._username = urlparser.unquote(value) if value else value
value = result.password
self._password = urlparser.unquote(value) if value else value
self._host = result.hostname
self._port = result.port
self._path = result.path or None
self._fragment = result.fragment or None
self._query = self._parse_string_into_query(result.query)
elif isinstance(url, Url):
self._copy_from_url(url)
elif url in (None, Default):
self._query = {}
else:
raise TypeError(f"Type passed into Url(...) is not a str/Url/None, but ({type(url)}).")
if scheme is not Default:
self._scheme = scheme
if username is not Default:
self._username = username
if password is not Default:
self._password = password
if host is not Default:
self._host = host
if port is not Default:
self._port = port
if path is not Default:
self._path = path
if fragment is not Default:
self._fragment = fragment
if query is not Default:
self._set_query(query)
# ----------------------------------------
# --------- Basic Url attributes ---------
@property
def scheme(self) -> Optional[str]:
return self._scheme
@property
def username(self) -> Optional[str]:
return self._username
@property
def password(self) -> Optional[str]:
return self._password
@property
def host(self) -> Optional[str]:
return self._host
@property
def port(self) -> Optional[int]:
return self._port
@property
def path(self) -> Optional[str]:
"""Path's can have variable placeholders in them, for more details see
[Path Formatting Placeholders](#path-formatting-placeholders).
"""
return self._path
@property
def fragment(self) -> Optional[str]:
return self._fragment
@property
def query(self) -> MappingProxyType:
"""Returns a live-updating mapping of the url query values as a read-only proxy map."""
# Mapping proxy is a read-only view of the passed in dict.
# This will LIVE update the mapping if _query is directly changed!
return MappingProxyType(self._query)
def query_value(self, key: str) -> Optional[QueryValue]:
""" Returns the value in query assigned to key, if key not found returns `None`. """
return self._query.get(key, None)
@scheme.setter
def scheme(self, value: Optional[str]):
self._scheme = value
pass
@username.setter
def username(self, value: Optional[str]):
self._username = value
pass
@password.setter
def password(self, value: Optional[str]):
self._password = value
pass
@host.setter
def host(self, value: Optional[str]):
self._host = value
pass
@port.setter
def port(self, value: Optional[int]):
self._port = value
pass
@path.setter
def path(self, value: Optional[str]):
"""Path's can have variable placeholders in them, for more details see
[Path Formatting Placeholders](#path-formatting-placeholders).
"""
self._path = value
self._path_changed()
pass
@fragment.setter
def fragment(self, value: Optional[str]):
self._fragment = value
pass
@query.setter
def query(self, value: Query):
"""Each value in query dictionary can be either a str/int, or a list of str/int.
If it is a list, see self.formatting_options for details on what happens.
"""
# Filter out None values from dictionary.
self._set_query(value)
# ---------------------------------
# --------- Configuration ---------
@property
def singular(self) -> Optional[bool]:
"""This is a hint to the underlying system that the query will return a singular object
instead of potentially many. If this is None, then underlying system may try to
inspect results to try and determine if it's many or one/singular.
todo: Implement 'None' value in api.RestClient, right now it treats None as False.
"""
return self._singular
@singular.setter
def singular(self, value: Optional[bool]):
warn(
"Url.singular deprecated pending a more generalized way to include metadata.",
DeprecationWarning, 2
)
self._singular = value
@property
def formatting_options(self) -> UrlFormattingOptions:
"""
This tells Url how to encode/decode multiple values [ie: list] for a particular
query name-key.
If `formatting_options` is None, then by default `DefaultQueryValueListFormat`
will be used.
Just like other Url attributes, if `formatting_options` is None, it will and you
append this Url to another Url, `formatting_options` will not change.
A query value can be either a str/int, or a list of str/int.
If it is a list of str/int, and if self.formatting_options is True,
then the key will have a suffix appended [`__in` by default] and the values will
be delimited by a string [`,` - comma by default].
If self.formatting_options is None, we instead duplicate the key
for each value in the list in final url.
Examples for `key='someKey', value=['A', 'B']`:
- self.formatting_options is None:
`?someKey=A&someKey=B`
- self.formatting_options is QueryValueListFormat('__in', ','):
`?someKey__in=A,B`.
"""
return self._formatting_options
@formatting_options.setter
def formatting_options(self, value: UrlFormattingOptions):
self._formatting_options = value
@property
def methods(self) -> Optional[tuple]:
# Return a read-only copy of the methods.
return tuple(self._methods)
@methods.setter
def methods(self, value: Union[Iterable[str], str]):
warn(
"Url.methods deprecated pending a more generalized way to include metadata.",
DeprecationWarning, 2
)
self._set_methods(value)
def methods_contain(self, method: str) -> bool:
"""Returns True if method is one of my methods, or if I have not assigned methods.
Otherwise, returns False.
It's faster to use this method then to get all methods and look yourself since I
use a set internally and can more quickly lookup things in the set.
"""
if not self._methods:
return True
return method in self._methods
def methods_have_one_in(self, methods: Set[str]) -> bool:
return not self._methods.isdisjoint(methods or set())
@property
def secondary_values(self) -> SecondaryValues:
"""Returns any SecondaryValues that have been attached to Url.
See `self.is_valid(...)` and/or `self.url(...)` for more details.
"""
return self._secondary_values
# ------------------------------
# --------- Copy/Utils ---------
def copy(self) -> Url:
""" We are immutable, so return self without needing to copy. """
return Url(self)
def copy_mutable(self) -> Url:
""" Returns a copy of self as a Url. """
return Url(self)
def __copy__(self):
# Creates a new object of type self, with self as first param.
# This should make a copy of self.
return Url(self)
# ---------------------------------
# ------------ Methods ------------
# todo: Not sure if this really should be here or not...
def query_id_if_singular(self):
val = self._query.get('id')
if val is None:
return None
if isinstance(val, (str, int)):
return val
new_val = list(map(str, val))
if len(new_val) == 1:
return new_val[0]
return None
def is_valid(
self, secondary_values: SecondaryValues = Default, attach_values: bool = False
) -> bool:
"""
A more efficient way to determine if we can produce a valid url string.
The efficiency comes from not needing to actually format and produce the url string.
Return True if self.url() would return a valid url, else False.
The Url version of this method allows you to 'attach' the secondary values
to the url. If so, will use the attached values when calling self.url(...) and passing
no secondary values into that method.
Args:
secondary_values: If query does not have a needed value to satisfy a formatting
placeholder, we will look at values in here.
It can be a object, dict or a sequence of objects.
If the object(s) or dict has the needed value, then we will use that to satisfy
the url placeholder.
attach_values: If `True`, will 'attach' the secondary values **IF** url is valid with
them.
Attaching means that they are the default secondary values used when generating
the Url or when calling is_valid(...) in the future.
If attach_values is a Class/Object, it will directly set this value into the new
copy.
We will do a shallow-copy IF the value is a dict/list.
Returns:
True: `Url` is valid and you can call `Url.url` without a problem
(with same secondary_values if you did not attach them).
False: `Url` is invalid, it needs more formatting params or there is some other issue.
"""
format_map = self._formatted_map(secondary_values=secondary_values)
if format_map and attach_values:
# Do a shallow copy if it's a list/dict, like we document we do.
# We don't make a copy for anything else (like a normal object).
if isinstance(secondary_values, (list, dict)):
secondary_values = copy(secondary_values)
# Attach these as the default secondary_values when formatting self.url()
self._secondary_values = secondary_values
return format_map is not None
def url(
self,
*,
default_scheme: str = None,
secondary_values: SecondaryValues = Default,
allow_invalid_url: bool = False,
):
warn(
"Url.url is deprecated, instead use `Url.format` "
"or directly convert url object to a str.",
DeprecationWarning, 2
)
return self.format(
default_scheme=default_scheme,
secondary_values=secondary_values,
allow_invalid_url=allow_invalid_url
)
def format(
self,
*,
default_scheme: str = None,
secondary_values: SecondaryValues = Default,
allow_invalid_url: bool = False,
) -> Optional[str]:
"""
Returns url as a string, with an optional `default_scheme`.
You can get the same value (with all args left at their default values)
by simply converting object to a string:
```python
some_url = Url("https://www.some-url.com/")
# Ways to convert to string:
print(some_url.format())
print(str(some_url))
print(f'Formatted Url: {some_url}')
```
Output:
```
https://www.some-url.com/
https://www.some-url.com/
Formatted Url: https://www.some-url.com/
```
Path's can have variable placeholders in them, for more details see
[Path Formatting Placeholders](#path-formatting-placeholders)
If any placeholder can't find it's value, we return `None`.
If you always need a string, pass False to the `format_placeholders` argument.
At some point in the future I'll probably support place-holders for
query-key/values. The `format_placeholders` argument would also control
this too once we get around to supporting that.
Args:
default_scheme: If self.scheme is None or blank, use this passed in default scheme.
secondary_values: If path needs formatting and there is no valid query value I can use,
see if obj has an attribute with a valid value. The attribute name will be same as
formatting key name.
If `None`, will check to see if we have any attached self.secondary_values and
try to use that if it has something when needed.
allow_invalid_url: If `True`: Always return a `str` with as much as the url formatted
as possible. We will never return None in this case.
If `False` (default): Always return a fully formatted url, and if we can't do
that then return `None` instead.
.. important:: If you indirectly convert a `Url` object into a string via
`f"some url {url_obj}"`, that will pass True to `allow_invalid_url`
in order to always produce a non-None string, as Python requires this.
This is mostly used when logging url objects.
Code that always requires a valid url should use this method to explicitly
format the `Url` object into a fully-formatted url string!
Returns:
Normal string as a fully constructed url.
If `None` is returned, the Url could not be formatted (see `Url.is_valid`).
There is a missing or invalid value for one of the required formatting key-names.
See Url class doc section
[Path Formatting Placeholders](#path-formatting-placeholders).
for more details.
"""
used_query_keys: List[str] = []
path = self._formatted_path(
secondary_values=secondary_values, query_keys_used=used_query_keys
)
# If we can't format the path due to finding all placeholders,
# then we return None to indicate we can't format the url.
if path is None:
if not allow_invalid_url:
return None
# Use unformatted / raw url path as we can't format it and we have allow_invalid_url.
path = self.path
netloc = ''
if self._host is not None:
netloc = self._host
userpass = None
if self._username is not None:
userpass = urlparser.quote(self._username)
if self._password is not None:
userpass = f"{userpass}:{urlparser.quote(self._password)}"
if userpass is not None:
netloc = f"{userpass}@{netloc}"
if self._port is not None:
netloc = f"{netloc}:{self._port}"
scheme = self._scheme or default_scheme or ''
fragment = self._fragment or ''
query_dict = self._query
if used_query_keys:
# Filter out any keys that we should ignore.
# Filters dict and makes a copy at the same time, exactly what we need!
query_dict = {k: query_dict[k] for k in query_dict if k not in used_query_keys}
query = self._format_query_into_string(query_dict)
return urlparser.urlunparse((scheme, netloc, path or '', '', query, fragment))
def __str__(self):
"""
Will return self.url(), which should return a formatted url string.
If that returns None, it means we have some part of the Url that can't be formatted,
so we then return an unformatted/raw version of the Url.
This means placeholders like `hello/{some_value}/`, if we don't have a value
for `{some_value}` then we would have to return the Url with the placeholder as-is
instead of formatting the Url and replacing `{some_value}` with the real value.
"""
# If we can't generate a valid url, then return the best invalid version we have.
return self.url(allow_invalid_url=True)
def __repr__(self):
return f"{type(self).__name__}('{self}')"
# ---------------------------------------------
# --------- Advanced Mutation Methods ---------
def set_methods(self, methods: Union[Iterable[str], str], *args):
"""Sets methods and returns self, so you can chain it with other mods.
You can pass in an iterable or a direct string, for any number of arguments.
We will iterate each argument if it's not a string to combine all of them
together in one list, which will be set on self.methods.
`self` is returned, so you can chain this with other method calls.
"""
self._set_methods(xloop(methods, args))
return self
def set_singular(self, value: Optional[bool]) -> Url:
"""Sets singular to value.
`self` is returned, so you can chain this with other method calls.
"""
self.singular = value
return self
def set_formatting_options(self, value: Optional[UrlFormattingOptions]) -> Url:
"""Does `self.formatting_options = value` for you while returning `self`.
`self` is returned, so you can easily chain this with other method calls if you wish.
"""
self.formatting_options = value
return self
def append_url(self, url: Optional[UrlStr]) -> Url:
"""Takes the components of url, and any that look true [ie: are not False or None]
are set on myself.
In the case of the path, `append_path` is called instead, so the path provided in
`url` is appended to any already existing path.
For the query, the `append_query()` is used, and only keys with the same key-name
will be replaced. Otherwise, the query key/value is added to any already existing
query key/value pairs.
For methods, `append_methods` is called, and will add any methods not already present.
`self` is returned, so you can chain this with other method calls.
"""
if url is None:
return self
# Make sure we have a Url, we want to look at it's various components...
url = Url.ensure_url(url, default_formatting_options=self.formatting_options)
self.append_path(url.path)
self.append_query(url._query)
self.append_methods(url._methods)
for attr_name in _attributes_to_set_when_appending:
other_value = getattr(url, attr_name, None)
if other_value is not None:
setattr(self, attr_name, other_value)
return self
def append_path(self, path_component: Optional[str]) -> Url:
"""Appends path with path_component, making sure to add a slash if needed to separate
new component from any current path.
`self` is returned, so you can chain this with other method calls.
"""
if not path_component:
return self
path_component = str(path_component)
if not path_component:
return self
# Remove any starting slash if needed, we will be providing our own slash later.
if path_component[0] == '/':
path_component = path_component[1:]
current_path = self._path
if not current_path:
current_path = ''
# If the current path ends in a slash, remove it.
if len(current_path) > 0 and current_path[-1] == '/':
current_path = current_path[:-1]
# Append the new path component with a slash to separate it.
self._path = f'{current_path}/{path_component}'
self._path_changed()
return self
def append_query(self, query: Optional[Query]) -> Url:
"""Calls `Url.query_add` for each key/value pair in query. Only keys with the
same key-name will be replaced. Otherwise, the query key/value is added to any already
existing query key/value pairs.
`self` is returned, so you can chain this with other method calls.
"""
if not query:
return self
for key, value in query.items():
self.query_add(key, value)
return self
def append_methods(self, methods: Iterable[str]) -> Url:
if methods:
self._methods.update(methods)
return self
def methods_add(self, method: str) -> Url:
if method:
self._methods.add(method)
return self
def methods_remove(self, method: str) -> Url:
if method:
self._methods.discard(method)
return self
def query_add(self, key: str = None, value: QueryValue = None) -> Url:
"""Use to set a key in query easily. This will entirely replace query with name `key`
if the value is not None. If value is None, nothing will happen/change.
If you want to remove a query item, use 'self.query_remove(...)' instead.
A query value can be either a str/int, or a list of str/int.
If it is a list, then see self.formatting_options for details on what happens.
A shallow copy will be made of value, ie: `copy(value)` before it's added to url.
`self` is returned, so you can chain this with other method calls.
"""
if value is None:
return self
if self._query is None:
self._query = {}
self._query[key] = copy(value)
return self
def query_remove(self, key) -> Url:
"""Remove value for key in query, if key does not exist nothing happens.
`self` is returned, so you can chain this with other method calls.
"""
self._query.pop(key, None)
return self
# ---------------------------
# --------- Private ---------
def _copy_from_url(self, url: Url):
"""Correctly copies in an optimized way, everything in url into self."""
self.__dict__.update(url.__dict__)
self._query = copy(url._query)
self._methods = copy(url._methods)
self._cached_format_keys = copy(url._cached_format_keys)
def _set_query(self, query: Query):
"""Makes _query the same as query, filter out None values and making a first-level deep
copy of the dict. This is needed in case we have lists of strings as a value.
"""
if query is None:
query = {}
# Filter out None values from dictionary.
self._query = {k: copy(v) for k, v in query.items() if v is not None}
def _set_methods(self, methods: Union[Iterable[str], str]):
if methods is None:
methods = tuple()
if isinstance(methods, str):
methods = (methods,)
self._methods = set(methods)
def _path_changed(self):
""" Called when path changes, mainly to reset format_keys cache. """
self._cached_format_keys = None
def _format_keys(self) -> List[str]:
"""Returns a list of format keys that are in the path. It caches this information
lazily. Be sure to call `Url._path_changed()` when the path changes, which throws away
this cached info.
"""
keys = self._cached_format_keys
if keys is not None:
return keys
path = self.path
if path:
keys = [t[1] for t in string.Formatter().parse(path) if t[1] is not None]
else:
keys = []
self._cached_format_keys = keys
return keys
def _parse_string_into_query(self, query_string: str) -> Query:
"""Go through each query value and parse it according to the self.formatting_options.
Keeps single-values as non-list items. If there is more than one value for a
particular query key-name, it will put all the values into a list.
"""
query = {}
formatting = self.formatting_options or DefaultQueryValueListFormat
delimiter = formatting.list_value_delimiter
suffix = formatting.list_key_suffix
suffix_len = len(suffix)
check_formatting = True if delimiter else False
for k, v in urlparser.parse_qs(query_string).items():
k: str
v: List[str]
all_values = []
force_list = False
is_list_keep_suffix = False
if check_formatting:
for keep_suffix in (formatting.list_key_suffixes_to_keep or []):
if k.endswith(keep_suffix):
is_list_keep_suffix = True
break
if check_formatting and (is_list_keep_suffix or k.endswith(suffix)):
# If we do have something like `__in=a`, we want to keep it as a list
# even if it only has a single item. That way when we construct the url
# again in the future, the `__in=a` will be preserved.
force_list = True
if not is_list_keep_suffix:
k = k[:-suffix_len]
for item in v:
all_values.extend(item.split(delimiter))
else:
all_values.extend(v)
existing_value = query.get(k)
if existing_value:
all_values.extend(xloop(existing_value))
if force_list or len(all_values) > 1:
query[k] = all_values
else:
query[k] = all_values[0]
return query
def _format_query_into_string(self, query: Query) -> str:
if not query:
return ""
options = self.formatting_options or DefaultQueryValueListFormat
query_filtered = {}
for key, val in query.items():
values = list(xloop(val))
if not values:
continue
# TODO: Probably remove this, seem strange.
formatter = values[0]
if not isinstance(formatter, _FormattedQueryValue):
formatter = _default_query_formatter
formatted_query = formatter.url_query(
key=key, url=self, url_options=options, values=values
)
query_filtered.update(formatted_query)
# It's legal to have query-values contain commas and so we declare them safe.
# This prevents them from being encoded (easier to read Url, makes them smaller).
return urlparser.urlencode(query_filtered, doseq=True, safe=',')
def _formatted_map(
self, secondary_values: SecondaryValues = Default, query_keys_used: List[str] = None
) -> Optional[dict]:
"""
Args:
secondary_values: Right now this method will return None [ie: invalid url]
if we have more than one
object passed into this. In the future, I want to support formatting placeholders
that support multiple objects. It's just not needed right now. But the interface
allows for it in the future, when we get around to support it.
If left as `xsentinels.Default` then we will use self.secondary_values,
if any are there. You can 'attach' secondary values to a Url via:
>>> Url.is_valid(secondary_values={'id': 2}, attach_values=True)
If `None`, then no secondary values will be used.
query_keys_used: List will be modified (if provided) with keys we used for placeholders
from the query-part of the url. Normally, you would remove the query-params
we used for this purpose before producing the final url.
Returns:
A dict/map of formatted query key/values.
"""
keys = self._format_keys()
if query_keys_used is None:
query_keys_used = []
if not keys:
return {}
if secondary_values is Default:
secondary_values = self._secondary_values
format_map = {}
query = self._query
for k in keys:
query_value = query.get(k)
# TODO: ********
# TODO: ******** Look for a single item in query_value, and if so use that?!?
# TODO: ********
if not isinstance(query_value, str):
query_value = list(xloop(query_value))
if len(query_value) == 1:
query_value = query_value[0]
# We consider blank-strings same as 'None' for our purposes here.
if not query_value and isinstance(query_value, str):
query_value = None
if query_value is not None and not isinstance(query_value, list):
# todo: convert any dates via .isoformat() ?
format_map[k] = str(query_value)
query_keys_used.append(k)
continue
if secondary_values:
if hasattr(secondary_values, k):
obj_value = getattr(secondary_values, k)
elif isinstance(secondary_values, dict):
obj_value = secondary_values.get(k)
else:
all_objs = list(xloop(secondary_values))
if len(all_objs) == 1 and hasattr(all_objs[0], k):
obj_value = getattr(all_objs[0], k)
else:
obj_value = None
if obj_value is not None and not isinstance(obj_value, list):
format_map[k] = str(obj_value)
continue
return None
return format_map
def _formatted_path(
self, secondary_values: Union[object, dict] = None, query_keys_used: List[str] = None
) -> Optional[str]:
"""
Formats my path and returns that. If the path could not be formatted returns False.
If this returns False it means a missing or invalid value for the formatting key-name.
A valid value is a str or int; NOT a list.
In the future, I'll support dates and auto-convert them into a string. But for right
now the value must be an int or str.
Args:
secondary_values: If passed in, and self.query does not have a valid value, will check
to see
if attribute with same format-key-name exists as an attribute on this object
or a key if it's a dict. If the value is NOT a list, it will be used. Otherwise,
it will be ignored as an invalid value.
query_keys_used: If you pass in a list, and I use a query as a value for
formatting the path, I will
put the key of each query I use inside this list. Normally you would not output
the query anymore [since it's inside the path now], but you can do whatever you
want with them.
Returns:
String if we have a valid path, otherwise False if we can't generate Path due to
lack of values in `Url.query` or `secondary_values`.
"""
mapped_values = self._formatted_map(
secondary_values=secondary_values, query_keys_used=query_keys_used
)
if mapped_values is None:
# We were unable to find values for one ore more format placeholders for our url path.
return None
# Url encode/quote all the values into a new dict.
mapped_values = {k: urlparser.quote(v, safe='') for (k, v) in mapped_values.items()}
path = self._path or ''
if mapped_values:
return path.format_map(mapped_values)
return path
_formatting_options: Optional[UrlFormattingOptions] = None
"""
If the value is `None`, it means we use DefaultQueryValueListFormat when constructing the
url.
BUT it also means we DON'T append this value into another Url via `Url.append_url(...)`,
we don't want to let this value ever override another value when appending Url's.
"""
_singular: Optional[bool] = None
_scheme: Optional[str] = None
_username: Optional[str] = None
_password: Optional[str] = None
_host: Optional[str] = None
_port: Optional[int] = None
_path: Optional[str] = None
_fragment: Optional[str] = None
# These are always set by init method, to ensure that they are never `None`.
_query: Query
_methods: Set[str]
# Others
_cached_format_keys: Optional[List[str]] = None
_secondary_values: SecondaryValues = None
UrlStr = Url | str | None
""" A type that can be either a string or Url.
"""
# Basically, we have to set every attribute except query/path/methods.
# For path we can append to the end of it.
# For query, we can merge them together since they are Dict's.
# For methods, we can merge them since they are lists.
_attributes_to_set_when_appending = {
'scheme',
'username',
'password',
'host',
'port',
'fragment',
'singular',
'formatting_options',
}
Global variables
var DefaultQueryValueListFormat
-
The default if
formatting_options
is None.Produces a format that does this when you have a query-key with multiple values: "http://host/path?key-name__in=value1,value2"
When you do this:
query_add('key-name', ['value1', 'value2'])
var DuplicateKeyNamesQueryValueListFormat
-
Produces a format that does this when you have a query-key with multiple values: "http://host/path?key-name=value1&key-name=value2"
When you do this:
query_add('key-name', ['value1', 'value2']
var HTTPPatch
-
Represents http PATCH method.
var Query
-
Type that represents the entire query portion of a Url.
var QueryValue
-
A query value can be either a str, or a list of str.
If it is a list of str, then the key will have __in appended and the values will be delimited by a comma ',' if Url.use_in_operator_for_query_lists is True, ie:
?someKey__in=A,B
.Otherwise they will be duplicated, ie:
?someKey=A&someKey=B
.If Url.use_in_operator_for_query_lists is False, we instead duplicate the key for each value in the list in final url, ie: (1, 2) turns into: 'a_key=1&a_key=2'.
use_in_operator_for_query_lists is True by default.
var UrlStr
-
A type that can be either a string or Url.
Classes
class Url (url: Union[str, Url] = None, *, scheme: str = Default, username: str = Default, password: str = Default, host: str = Default, port: int = Default, path: str = Default, query: Query = Default, fragment: str = Default, formatting_options: UrlFormattingOptions = Default, singular: bool = None, methods: Union[str, Iterable[str]] = None)
-
Allows you to easily create, work with and append Urls together.
You can pass a normal Url or string into the first argument of
Url
, and if you do it as a string it will be parsed as a Url and it's components will be set into the returned as a Url with it's various components parsed into Url's various attributes.There is also a parameter for each Url component attribute in the
Url
method.If no value is provided for a particular url component, it will be assigned a
None
value.Use
Url.url()
method to get a urlstr
back from components.For more details on path features, see Path Formatting Placeholders
Mutating Methods
A big way to use Url is to
Url.append_url()
Url
's together. You can then take differentUrl
's that have different components and construct a finalUrl
to use.When appending a Url to another [via
Url.append_url
], if any particular component has a None value, it will not change the Url it's being appended to for that particular component. This allows you to append Urls together and only relevant/set-values will be whats appended. This allows you to construct Urls based on pieces of Urls to construct the final Url.When you append a Url, the path and query and methods are treated in a special way:
-
Url.path
: it will be appended to the destination Url viaUrl.append_path()
, and therefore, the path will be appended onto the existing path (with a/
if needed to separate them). -
Url.query
: Keys will be added viaUrl.query_add()
, and therefore the only keys that will be replaced are ones where the same key-name exists in both Urls. Otherwise, the keys are 'added' together. -
Url.methods
: This will get union'd with the existingmethods
in the destination url.
The methods that mutate self [such as 'append_path', etc] return self, so you can chain them together with other mutating methods.
Example Usages:
>>> urlc = Url() >>> urlc.scheme = 'http' >>> urlc.host = 'www.google.com' >>> urlc.url() "http://www.google.com" >>> urlc = Url("http://www.google.com").append_url("/hello") >>> urlc.url() "http://www.google.com/hello"
__init__
SpecificsPass in a single 'url' or pass in the individual components.
If you pass in both, the individual components [kw-args] will take precedence. If you pass in a query parameter arg, it will completely replace entire query from any that were provided in
url
string. Same goes with the 'path'. Use 'append_url(…)' to easily merge/append query/paths together.If
xsentinels.Default
is left in place, then that value will be ignored and not replace anything that was parsed for that component fromurl
string. Unless the 'url' pass in defined that particular component it will be set to None. When you append a Url on another with values set to None, it will not 'append' them into the Url.If you pass in a None, we will replace that particular value with a None inside the Url.
All parameters are optional. Providing no parameters at all creates a completely blank Url. If you ask for the .url() of a blank Url, an empty string is returned.
Here is an example of all of these basic Url components in their proper place:
scheme://username:password@host:port/path?query#fragment
Args
url
:str, Url
-
A normal Url formatted string or Url object; ie: "www.google.com/hello".
If provided, forms the basis of the Url. All other parameters will over-write that particular component of the url provided here [ie: the other parameters take priority].
scheme
- Replaces/Sets scheme on new Url object.
password
- Replaces/Sets password on new Url object.
host
- Replaces/Sets host on new Url object.
port
- Replaces/Sets port on new Url object.
path
- Replaces/Sets path on new Url object. Path's can have variable placeholders in them, see Path Formatting Placeholders.
fragment
- Replaces/Sets fragment on new Url object.
query
- Replaces/Sets query on new Url object.
formatting_options: Way to configure how a query value can be split up into a list when parsing a url, and back again when reconstructing the url into a string. For details, see the
UrlFormattingOptions
class.If set with None, <code><a title="xurls.url.Url" href="#xurls.url.Url">Url</a></code> class uses <code><a title="xurls.url.DefaultQueryValueListFormat" href="#xurls.url.DefaultQueryValueListFormat">DefaultQueryValueListFormat</a></code> by default for it's formatting options.
singular
- (deprecated: need more generalized way to include metadata) Provides a hint to indicate if the body of the request/response should be a list or a single object. If 'None' [default], then the underlying system will do it's best to guess based on other factors.
methods
- (deprecated: need more generalized way to include metadata) If useful, you can provide a hint of which HTTP methods this Url is valid for. Defaults to an empty set. When you append methods to another Url, the result is a union of both sets of methods from the two Url's.
Expand source code
class Url: """ Allows you to easily create, work with and append Urls together. You can pass a normal Url or string into the first argument of `Url.__init__`, and if you do it as a string it will be parsed as a Url and it's components will be set into the returned as a Url with it's various components parsed into Url's various attributes. There is also a parameter for each Url component attribute in the `Url.__init__` method. If no value is provided for a particular url component, it will be assigned a `None` value. Use `Url.url()` method to get a url `str` back from components. For more details on path features, see [Path Formatting Placeholders](#path-formatting-placeholders) ## Mutating Methods A big way to use Url is to `Url.append_url` `Url`'s together. You can then take different `Url`'s that have different components and construct a final `Url` to use. When appending a Url to another [via `Url.append_url`], if any particular component has a None value, it will not change the Url it's being appended to for that particular component. This allows you to append Urls together and only relevant/set-values will be whats appended. This allows you to construct Urls based on pieces of Urls to construct the final Url. When you append a Url, the path and query and methods are treated in a special way: - `Url.path`: it will be appended to the destination Url via `Url.append_path`, and therefore, the path will be appended onto the existing path (with a `/` if needed to separate them). - `Url.query`: Keys will be added via `Url.query_add`, and therefore the only keys that will be replaced are ones where the same key-name exists in both Urls. Otherwise, the keys are 'added' together. - `Url.methods`: This will get union'd with the existing `methods` in the destination url. The methods that mutate self [such as 'append_path', etc] return self, so you can chain them together with other mutating methods. Example Usages: >>> urlc = Url() >>> urlc.scheme = 'http' >>> urlc.host = 'www.google.com' >>> urlc.url() "http://www.google.com" >>> urlc = Url("http://www.google.com").append_url("/hello") >>> urlc.url() "http://www.google.com/hello" """ @classmethod def ensure_url( cls: Type[T], value: Optional[UrlStr], *, default_formatting_options: UrlFormattingOptions = Default, ) -> T: """Will check to see if the passed in value is a `Url` (depending on the class you call `Url.ensure_url` on). If so, will return value unchanged. This is an optimization and the reason to use this method over just constructing a new `Url` and passing the value into that. If the value is already a Url and it's formatting_options don't need to be set to `default_formatting_options` it will just return it without any extra work. If not then will create a new instance of cls and pass value to `__init__` and return result. ### If you provide `default_formatting_options`: If you pass in a `str` to this method, then any `formatting_options` you passed in will be checked and used if provided. If you pass in a Url, and it has not explicitly set formatting_options, we will make a copy of passed in Url and set `default_formatting_options` on it and return copy. Otherwise, Url will use the usual `DefaultQueryValueListFormat` for the default format. Args: value: You can pass in a UrlStr (which is either a `str` or `Url`), and you will get back a `Url` object from it. If the value is exactly the same as the class you call this class method on, you will get it back unchanged. If not then we create a new object of type `cls` and give value to it as a init parameter. If `value` is not a `Url` or `str`, we will try and convert it to a `str` for you in an attempt to extract a url string from the passed in object. default_formatting_options (UrlFormattingOptions): Formatting options to use IF passed in value is a string. If the passed in value is a Url object, we use whatever it has set on it. If no formatting options are set on the `Url` object that is passed in, and you pass in a default_formatting_options, we will copy passed in Url and set that formatting option on it, and return copy. Returns: Url: Object of same type as `cls`, which is the class you called `Url.ensure_url` on. """ if type(value) is cls and (not default_formatting_options or value.formatting_options): return value if isinstance(value, Url) and value.formatting_options: # Url already has explicitly set formatting-options, keep whatever it has. return cls(value) # Otherwise, we are some other non-Url value (ie: a `str` probably), # try tp convert value to string and any default_formatting_options the user provided. return cls(value, formatting_options=default_formatting_options) def __init__( self, url: Union[str, Url] = None, *, scheme: str = Default, username: str = Default, password: str = Default, host: str = Default, port: int = Default, path: str = Default, query: Query = Default, fragment: str = Default, formatting_options: UrlFormattingOptions = Default, # Params below are extra metadata about Url that is useful to communicate about: # TODO: Find a more generalized way to include 'mergeable'/'appendable' metadata # in the Url object; For now keeping them, but need to remove in the # next major release. singular: bool = None, methods: Union[str, Iterable[str]] = None, ): """ # `__init__` Specifics Pass in a single 'url' or pass in the individual components. If you pass in both, the individual components [kw-args] will take precedence. If you pass in a query parameter arg, it will completely replace entire query from any that were provided in `url` string. Same goes with the 'path'. Use 'append_url(...)' to easily merge/append query/paths together. If `xsentinels.Default` is left in place, then that value will be ignored and not replace anything that was parsed for that component from `url` string. Unless the 'url' pass in defined that particular component it will be set to None. When you append a Url on another with values set to None, it will not 'append' them into the Url. If you pass in a None, we will replace that particular value with a None inside the Url. All parameters are optional. Providing no parameters at all creates a completely blank Url. If you ask for the .url() of a blank Url, an empty string is returned. Here is an example of all of these basic Url components in their proper place: `scheme://username:password@host:port/path?query#fragment` Args: url (str, Url): A normal Url formatted string or Url object; ie: "www.google.com/hello". If provided, forms the basis of the Url. All other parameters will over-write that particular component of the url provided here [ie: the other parameters take priority]. scheme: Replaces/Sets scheme on new Url object. password: Replaces/Sets password on new Url object. host: Replaces/Sets host on new Url object. port: Replaces/Sets port on new Url object. path: Replaces/Sets path on new Url object. Path's can have variable placeholders in them, see [Path Formatting Placeholders](#path-formatting-placeholders). fragment: Replaces/Sets fragment on new Url object. query: Replaces/Sets query on new Url object. formatting_options: Way to configure how a query value can be split up into a list when parsing a url, and back again when reconstructing the url into a string. For details, see the `UrlFormattingOptions` class. If set with None, `Url` class uses `DefaultQueryValueListFormat` by default for it's formatting options. singular: (deprecated: need more generalized way to include metadata) Provides a hint to indicate if the body of the request/response should be a list or a single object. If 'None' [default], then the underlying system will do it's best to guess based on other factors. methods: (deprecated: need more generalized way to include metadata) If useful, you can provide a hint of which HTTP methods this Url is valid for. Defaults to an empty set. When you append methods to another Url, the result is a union of both sets of methods from the two Url's. """ if formatting_options is not Default: self._formatting_options = formatting_options if methods is not None: warn( "Url.methods deprecated pending a more generalized way to include metadata.", DeprecationWarning, 2 ) self._set_methods(methods) else: self._methods = set() if singular not in (None, Default): warn( "Url.singular deprecated pending a more generalized way to include metadata.", DeprecationWarning, 2 ) self._singular = singular if isinstance(url, str): result: urlparser.ParseResult = urlparser.urlparse(url) self._scheme = result.scheme or None value = result.username self._username = urlparser.unquote(value) if value else value value = result.password self._password = urlparser.unquote(value) if value else value self._host = result.hostname self._port = result.port self._path = result.path or None self._fragment = result.fragment or None self._query = self._parse_string_into_query(result.query) elif isinstance(url, Url): self._copy_from_url(url) elif url in (None, Default): self._query = {} else: raise TypeError(f"Type passed into Url(...) is not a str/Url/None, but ({type(url)}).") if scheme is not Default: self._scheme = scheme if username is not Default: self._username = username if password is not Default: self._password = password if host is not Default: self._host = host if port is not Default: self._port = port if path is not Default: self._path = path if fragment is not Default: self._fragment = fragment if query is not Default: self._set_query(query) # ---------------------------------------- # --------- Basic Url attributes --------- @property def scheme(self) -> Optional[str]: return self._scheme @property def username(self) -> Optional[str]: return self._username @property def password(self) -> Optional[str]: return self._password @property def host(self) -> Optional[str]: return self._host @property def port(self) -> Optional[int]: return self._port @property def path(self) -> Optional[str]: """Path's can have variable placeholders in them, for more details see [Path Formatting Placeholders](#path-formatting-placeholders). """ return self._path @property def fragment(self) -> Optional[str]: return self._fragment @property def query(self) -> MappingProxyType: """Returns a live-updating mapping of the url query values as a read-only proxy map.""" # Mapping proxy is a read-only view of the passed in dict. # This will LIVE update the mapping if _query is directly changed! return MappingProxyType(self._query) def query_value(self, key: str) -> Optional[QueryValue]: """ Returns the value in query assigned to key, if key not found returns `None`. """ return self._query.get(key, None) @scheme.setter def scheme(self, value: Optional[str]): self._scheme = value pass @username.setter def username(self, value: Optional[str]): self._username = value pass @password.setter def password(self, value: Optional[str]): self._password = value pass @host.setter def host(self, value: Optional[str]): self._host = value pass @port.setter def port(self, value: Optional[int]): self._port = value pass @path.setter def path(self, value: Optional[str]): """Path's can have variable placeholders in them, for more details see [Path Formatting Placeholders](#path-formatting-placeholders). """ self._path = value self._path_changed() pass @fragment.setter def fragment(self, value: Optional[str]): self._fragment = value pass @query.setter def query(self, value: Query): """Each value in query dictionary can be either a str/int, or a list of str/int. If it is a list, see self.formatting_options for details on what happens. """ # Filter out None values from dictionary. self._set_query(value) # --------------------------------- # --------- Configuration --------- @property def singular(self) -> Optional[bool]: """This is a hint to the underlying system that the query will return a singular object instead of potentially many. If this is None, then underlying system may try to inspect results to try and determine if it's many or one/singular. todo: Implement 'None' value in api.RestClient, right now it treats None as False. """ return self._singular @singular.setter def singular(self, value: Optional[bool]): warn( "Url.singular deprecated pending a more generalized way to include metadata.", DeprecationWarning, 2 ) self._singular = value @property def formatting_options(self) -> UrlFormattingOptions: """ This tells Url how to encode/decode multiple values [ie: list] for a particular query name-key. If `formatting_options` is None, then by default `DefaultQueryValueListFormat` will be used. Just like other Url attributes, if `formatting_options` is None, it will and you append this Url to another Url, `formatting_options` will not change. A query value can be either a str/int, or a list of str/int. If it is a list of str/int, and if self.formatting_options is True, then the key will have a suffix appended [`__in` by default] and the values will be delimited by a string [`,` - comma by default]. If self.formatting_options is None, we instead duplicate the key for each value in the list in final url. Examples for `key='someKey', value=['A', 'B']`: - self.formatting_options is None: `?someKey=A&someKey=B` - self.formatting_options is QueryValueListFormat('__in', ','): `?someKey__in=A,B`. """ return self._formatting_options @formatting_options.setter def formatting_options(self, value: UrlFormattingOptions): self._formatting_options = value @property def methods(self) -> Optional[tuple]: # Return a read-only copy of the methods. return tuple(self._methods) @methods.setter def methods(self, value: Union[Iterable[str], str]): warn( "Url.methods deprecated pending a more generalized way to include metadata.", DeprecationWarning, 2 ) self._set_methods(value) def methods_contain(self, method: str) -> bool: """Returns True if method is one of my methods, or if I have not assigned methods. Otherwise, returns False. It's faster to use this method then to get all methods and look yourself since I use a set internally and can more quickly lookup things in the set. """ if not self._methods: return True return method in self._methods def methods_have_one_in(self, methods: Set[str]) -> bool: return not self._methods.isdisjoint(methods or set()) @property def secondary_values(self) -> SecondaryValues: """Returns any SecondaryValues that have been attached to Url. See `self.is_valid(...)` and/or `self.url(...)` for more details. """ return self._secondary_values # ------------------------------ # --------- Copy/Utils --------- def copy(self) -> Url: """ We are immutable, so return self without needing to copy. """ return Url(self) def copy_mutable(self) -> Url: """ Returns a copy of self as a Url. """ return Url(self) def __copy__(self): # Creates a new object of type self, with self as first param. # This should make a copy of self. return Url(self) # --------------------------------- # ------------ Methods ------------ # todo: Not sure if this really should be here or not... def query_id_if_singular(self): val = self._query.get('id') if val is None: return None if isinstance(val, (str, int)): return val new_val = list(map(str, val)) if len(new_val) == 1: return new_val[0] return None def is_valid( self, secondary_values: SecondaryValues = Default, attach_values: bool = False ) -> bool: """ A more efficient way to determine if we can produce a valid url string. The efficiency comes from not needing to actually format and produce the url string. Return True if self.url() would return a valid url, else False. The Url version of this method allows you to 'attach' the secondary values to the url. If so, will use the attached values when calling self.url(...) and passing no secondary values into that method. Args: secondary_values: If query does not have a needed value to satisfy a formatting placeholder, we will look at values in here. It can be a object, dict or a sequence of objects. If the object(s) or dict has the needed value, then we will use that to satisfy the url placeholder. attach_values: If `True`, will 'attach' the secondary values **IF** url is valid with them. Attaching means that they are the default secondary values used when generating the Url or when calling is_valid(...) in the future. If attach_values is a Class/Object, it will directly set this value into the new copy. We will do a shallow-copy IF the value is a dict/list. Returns: True: `Url` is valid and you can call `Url.url` without a problem (with same secondary_values if you did not attach them). False: `Url` is invalid, it needs more formatting params or there is some other issue. """ format_map = self._formatted_map(secondary_values=secondary_values) if format_map and attach_values: # Do a shallow copy if it's a list/dict, like we document we do. # We don't make a copy for anything else (like a normal object). if isinstance(secondary_values, (list, dict)): secondary_values = copy(secondary_values) # Attach these as the default secondary_values when formatting self.url() self._secondary_values = secondary_values return format_map is not None def url( self, *, default_scheme: str = None, secondary_values: SecondaryValues = Default, allow_invalid_url: bool = False, ): warn( "Url.url is deprecated, instead use `Url.format` " "or directly convert url object to a str.", DeprecationWarning, 2 ) return self.format( default_scheme=default_scheme, secondary_values=secondary_values, allow_invalid_url=allow_invalid_url ) def format( self, *, default_scheme: str = None, secondary_values: SecondaryValues = Default, allow_invalid_url: bool = False, ) -> Optional[str]: """ Returns url as a string, with an optional `default_scheme`. You can get the same value (with all args left at their default values) by simply converting object to a string: ```python some_url = Url("https://www.some-url.com/") # Ways to convert to string: print(some_url.format()) print(str(some_url)) print(f'Formatted Url: {some_url}') ``` Output: ``` https://www.some-url.com/ https://www.some-url.com/ Formatted Url: https://www.some-url.com/ ``` Path's can have variable placeholders in them, for more details see [Path Formatting Placeholders](#path-formatting-placeholders) If any placeholder can't find it's value, we return `None`. If you always need a string, pass False to the `format_placeholders` argument. At some point in the future I'll probably support place-holders for query-key/values. The `format_placeholders` argument would also control this too once we get around to supporting that. Args: default_scheme: If self.scheme is None or blank, use this passed in default scheme. secondary_values: If path needs formatting and there is no valid query value I can use, see if obj has an attribute with a valid value. The attribute name will be same as formatting key name. If `None`, will check to see if we have any attached self.secondary_values and try to use that if it has something when needed. allow_invalid_url: If `True`: Always return a `str` with as much as the url formatted as possible. We will never return None in this case. If `False` (default): Always return a fully formatted url, and if we can't do that then return `None` instead. .. important:: If you indirectly convert a `Url` object into a string via `f"some url {url_obj}"`, that will pass True to `allow_invalid_url` in order to always produce a non-None string, as Python requires this. This is mostly used when logging url objects. Code that always requires a valid url should use this method to explicitly format the `Url` object into a fully-formatted url string! Returns: Normal string as a fully constructed url. If `None` is returned, the Url could not be formatted (see `Url.is_valid`). There is a missing or invalid value for one of the required formatting key-names. See Url class doc section [Path Formatting Placeholders](#path-formatting-placeholders). for more details. """ used_query_keys: List[str] = [] path = self._formatted_path( secondary_values=secondary_values, query_keys_used=used_query_keys ) # If we can't format the path due to finding all placeholders, # then we return None to indicate we can't format the url. if path is None: if not allow_invalid_url: return None # Use unformatted / raw url path as we can't format it and we have allow_invalid_url. path = self.path netloc = '' if self._host is not None: netloc = self._host userpass = None if self._username is not None: userpass = urlparser.quote(self._username) if self._password is not None: userpass = f"{userpass}:{urlparser.quote(self._password)}" if userpass is not None: netloc = f"{userpass}@{netloc}" if self._port is not None: netloc = f"{netloc}:{self._port}" scheme = self._scheme or default_scheme or '' fragment = self._fragment or '' query_dict = self._query if used_query_keys: # Filter out any keys that we should ignore. # Filters dict and makes a copy at the same time, exactly what we need! query_dict = {k: query_dict[k] for k in query_dict if k not in used_query_keys} query = self._format_query_into_string(query_dict) return urlparser.urlunparse((scheme, netloc, path or '', '', query, fragment)) def __str__(self): """ Will return self.url(), which should return a formatted url string. If that returns None, it means we have some part of the Url that can't be formatted, so we then return an unformatted/raw version of the Url. This means placeholders like `hello/{some_value}/`, if we don't have a value for `{some_value}` then we would have to return the Url with the placeholder as-is instead of formatting the Url and replacing `{some_value}` with the real value. """ # If we can't generate a valid url, then return the best invalid version we have. return self.url(allow_invalid_url=True) def __repr__(self): return f"{type(self).__name__}('{self}')" # --------------------------------------------- # --------- Advanced Mutation Methods --------- def set_methods(self, methods: Union[Iterable[str], str], *args): """Sets methods and returns self, so you can chain it with other mods. You can pass in an iterable or a direct string, for any number of arguments. We will iterate each argument if it's not a string to combine all of them together in one list, which will be set on self.methods. `self` is returned, so you can chain this with other method calls. """ self._set_methods(xloop(methods, args)) return self def set_singular(self, value: Optional[bool]) -> Url: """Sets singular to value. `self` is returned, so you can chain this with other method calls. """ self.singular = value return self def set_formatting_options(self, value: Optional[UrlFormattingOptions]) -> Url: """Does `self.formatting_options = value` for you while returning `self`. `self` is returned, so you can easily chain this with other method calls if you wish. """ self.formatting_options = value return self def append_url(self, url: Optional[UrlStr]) -> Url: """Takes the components of url, and any that look true [ie: are not False or None] are set on myself. In the case of the path, `append_path` is called instead, so the path provided in `url` is appended to any already existing path. For the query, the `append_query()` is used, and only keys with the same key-name will be replaced. Otherwise, the query key/value is added to any already existing query key/value pairs. For methods, `append_methods` is called, and will add any methods not already present. `self` is returned, so you can chain this with other method calls. """ if url is None: return self # Make sure we have a Url, we want to look at it's various components... url = Url.ensure_url(url, default_formatting_options=self.formatting_options) self.append_path(url.path) self.append_query(url._query) self.append_methods(url._methods) for attr_name in _attributes_to_set_when_appending: other_value = getattr(url, attr_name, None) if other_value is not None: setattr(self, attr_name, other_value) return self def append_path(self, path_component: Optional[str]) -> Url: """Appends path with path_component, making sure to add a slash if needed to separate new component from any current path. `self` is returned, so you can chain this with other method calls. """ if not path_component: return self path_component = str(path_component) if not path_component: return self # Remove any starting slash if needed, we will be providing our own slash later. if path_component[0] == '/': path_component = path_component[1:] current_path = self._path if not current_path: current_path = '' # If the current path ends in a slash, remove it. if len(current_path) > 0 and current_path[-1] == '/': current_path = current_path[:-1] # Append the new path component with a slash to separate it. self._path = f'{current_path}/{path_component}' self._path_changed() return self def append_query(self, query: Optional[Query]) -> Url: """Calls `Url.query_add` for each key/value pair in query. Only keys with the same key-name will be replaced. Otherwise, the query key/value is added to any already existing query key/value pairs. `self` is returned, so you can chain this with other method calls. """ if not query: return self for key, value in query.items(): self.query_add(key, value) return self def append_methods(self, methods: Iterable[str]) -> Url: if methods: self._methods.update(methods) return self def methods_add(self, method: str) -> Url: if method: self._methods.add(method) return self def methods_remove(self, method: str) -> Url: if method: self._methods.discard(method) return self def query_add(self, key: str = None, value: QueryValue = None) -> Url: """Use to set a key in query easily. This will entirely replace query with name `key` if the value is not None. If value is None, nothing will happen/change. If you want to remove a query item, use 'self.query_remove(...)' instead. A query value can be either a str/int, or a list of str/int. If it is a list, then see self.formatting_options for details on what happens. A shallow copy will be made of value, ie: `copy(value)` before it's added to url. `self` is returned, so you can chain this with other method calls. """ if value is None: return self if self._query is None: self._query = {} self._query[key] = copy(value) return self def query_remove(self, key) -> Url: """Remove value for key in query, if key does not exist nothing happens. `self` is returned, so you can chain this with other method calls. """ self._query.pop(key, None) return self # --------------------------- # --------- Private --------- def _copy_from_url(self, url: Url): """Correctly copies in an optimized way, everything in url into self.""" self.__dict__.update(url.__dict__) self._query = copy(url._query) self._methods = copy(url._methods) self._cached_format_keys = copy(url._cached_format_keys) def _set_query(self, query: Query): """Makes _query the same as query, filter out None values and making a first-level deep copy of the dict. This is needed in case we have lists of strings as a value. """ if query is None: query = {} # Filter out None values from dictionary. self._query = {k: copy(v) for k, v in query.items() if v is not None} def _set_methods(self, methods: Union[Iterable[str], str]): if methods is None: methods = tuple() if isinstance(methods, str): methods = (methods,) self._methods = set(methods) def _path_changed(self): """ Called when path changes, mainly to reset format_keys cache. """ self._cached_format_keys = None def _format_keys(self) -> List[str]: """Returns a list of format keys that are in the path. It caches this information lazily. Be sure to call `Url._path_changed()` when the path changes, which throws away this cached info. """ keys = self._cached_format_keys if keys is not None: return keys path = self.path if path: keys = [t[1] for t in string.Formatter().parse(path) if t[1] is not None] else: keys = [] self._cached_format_keys = keys return keys def _parse_string_into_query(self, query_string: str) -> Query: """Go through each query value and parse it according to the self.formatting_options. Keeps single-values as non-list items. If there is more than one value for a particular query key-name, it will put all the values into a list. """ query = {} formatting = self.formatting_options or DefaultQueryValueListFormat delimiter = formatting.list_value_delimiter suffix = formatting.list_key_suffix suffix_len = len(suffix) check_formatting = True if delimiter else False for k, v in urlparser.parse_qs(query_string).items(): k: str v: List[str] all_values = [] force_list = False is_list_keep_suffix = False if check_formatting: for keep_suffix in (formatting.list_key_suffixes_to_keep or []): if k.endswith(keep_suffix): is_list_keep_suffix = True break if check_formatting and (is_list_keep_suffix or k.endswith(suffix)): # If we do have something like `__in=a`, we want to keep it as a list # even if it only has a single item. That way when we construct the url # again in the future, the `__in=a` will be preserved. force_list = True if not is_list_keep_suffix: k = k[:-suffix_len] for item in v: all_values.extend(item.split(delimiter)) else: all_values.extend(v) existing_value = query.get(k) if existing_value: all_values.extend(xloop(existing_value)) if force_list or len(all_values) > 1: query[k] = all_values else: query[k] = all_values[0] return query def _format_query_into_string(self, query: Query) -> str: if not query: return "" options = self.formatting_options or DefaultQueryValueListFormat query_filtered = {} for key, val in query.items(): values = list(xloop(val)) if not values: continue # TODO: Probably remove this, seem strange. formatter = values[0] if not isinstance(formatter, _FormattedQueryValue): formatter = _default_query_formatter formatted_query = formatter.url_query( key=key, url=self, url_options=options, values=values ) query_filtered.update(formatted_query) # It's legal to have query-values contain commas and so we declare them safe. # This prevents them from being encoded (easier to read Url, makes them smaller). return urlparser.urlencode(query_filtered, doseq=True, safe=',') def _formatted_map( self, secondary_values: SecondaryValues = Default, query_keys_used: List[str] = None ) -> Optional[dict]: """ Args: secondary_values: Right now this method will return None [ie: invalid url] if we have more than one object passed into this. In the future, I want to support formatting placeholders that support multiple objects. It's just not needed right now. But the interface allows for it in the future, when we get around to support it. If left as `xsentinels.Default` then we will use self.secondary_values, if any are there. You can 'attach' secondary values to a Url via: >>> Url.is_valid(secondary_values={'id': 2}, attach_values=True) If `None`, then no secondary values will be used. query_keys_used: List will be modified (if provided) with keys we used for placeholders from the query-part of the url. Normally, you would remove the query-params we used for this purpose before producing the final url. Returns: A dict/map of formatted query key/values. """ keys = self._format_keys() if query_keys_used is None: query_keys_used = [] if not keys: return {} if secondary_values is Default: secondary_values = self._secondary_values format_map = {} query = self._query for k in keys: query_value = query.get(k) # TODO: ******** # TODO: ******** Look for a single item in query_value, and if so use that?!? # TODO: ******** if not isinstance(query_value, str): query_value = list(xloop(query_value)) if len(query_value) == 1: query_value = query_value[0] # We consider blank-strings same as 'None' for our purposes here. if not query_value and isinstance(query_value, str): query_value = None if query_value is not None and not isinstance(query_value, list): # todo: convert any dates via .isoformat() ? format_map[k] = str(query_value) query_keys_used.append(k) continue if secondary_values: if hasattr(secondary_values, k): obj_value = getattr(secondary_values, k) elif isinstance(secondary_values, dict): obj_value = secondary_values.get(k) else: all_objs = list(xloop(secondary_values)) if len(all_objs) == 1 and hasattr(all_objs[0], k): obj_value = getattr(all_objs[0], k) else: obj_value = None if obj_value is not None and not isinstance(obj_value, list): format_map[k] = str(obj_value) continue return None return format_map def _formatted_path( self, secondary_values: Union[object, dict] = None, query_keys_used: List[str] = None ) -> Optional[str]: """ Formats my path and returns that. If the path could not be formatted returns False. If this returns False it means a missing or invalid value for the formatting key-name. A valid value is a str or int; NOT a list. In the future, I'll support dates and auto-convert them into a string. But for right now the value must be an int or str. Args: secondary_values: If passed in, and self.query does not have a valid value, will check to see if attribute with same format-key-name exists as an attribute on this object or a key if it's a dict. If the value is NOT a list, it will be used. Otherwise, it will be ignored as an invalid value. query_keys_used: If you pass in a list, and I use a query as a value for formatting the path, I will put the key of each query I use inside this list. Normally you would not output the query anymore [since it's inside the path now], but you can do whatever you want with them. Returns: String if we have a valid path, otherwise False if we can't generate Path due to lack of values in `Url.query` or `secondary_values`. """ mapped_values = self._formatted_map( secondary_values=secondary_values, query_keys_used=query_keys_used ) if mapped_values is None: # We were unable to find values for one ore more format placeholders for our url path. return None # Url encode/quote all the values into a new dict. mapped_values = {k: urlparser.quote(v, safe='') for (k, v) in mapped_values.items()} path = self._path or '' if mapped_values: return path.format_map(mapped_values) return path _formatting_options: Optional[UrlFormattingOptions] = None """ If the value is `None`, it means we use DefaultQueryValueListFormat when constructing the url. BUT it also means we DON'T append this value into another Url via `Url.append_url(...)`, we don't want to let this value ever override another value when appending Url's. """ _singular: Optional[bool] = None _scheme: Optional[str] = None _username: Optional[str] = None _password: Optional[str] = None _host: Optional[str] = None _port: Optional[int] = None _path: Optional[str] = None _fragment: Optional[str] = None # These are always set by init method, to ensure that they are never `None`. _query: Query _methods: Set[str] # Others _cached_format_keys: Optional[List[str]] = None _secondary_values: SecondaryValues = None
Static methods
def ensure_url(value: Optional[UrlStr], *, default_formatting_options: UrlFormattingOptions = Default) ‑> ~T
-
Will check to see if the passed in value is a
Url
(depending on the class you callUrl.ensure_url()
on). If so, will return value unchanged. This is an optimization and the reason to use this method over just constructing a newUrl
and passing the value into that. If the value is already a Url and it's formatting_options don't need to be set todefault_formatting_options
it will just return it without any extra work.If not then will create a new instance of cls and pass value to <code>\_\_init\_\_</code> and return result. ### If you provide <code>default\_formatting\_options</code>: If you pass in a <code>str</code> to this method, then any <code>formatting\_options</code> you passed in will be checked and used if provided. If you pass in a Url, and it has not explicitly set formatting_options, we will make a copy of passed in Url and set <code>default\_formatting\_options</code> on it and return copy. Otherwise, Url will use the usual <code><a title="xurls.url.DefaultQueryValueListFormat" href="#xurls.url.DefaultQueryValueListFormat">DefaultQueryValueListFormat</a></code> for the default format.
Args
value
-
You can pass in a UrlStr (which is either a
str
orUrl
), and you will get back aUrl
object from it. If the value is exactly the same as the class you call this class method on, you will get it back unchanged. If not then we create a new object of typecls
and give value to it as a init parameter.If
value
is not aUrl
orstr
, we will try and convert it to astr
for you in an attempt to extract a url string from the passed in object. default_formatting_options
:UrlFormattingOptions
- Formatting options to use IF
passed in value is a string.
If the passed in value is a Url object, we use
whatever it has set on it. If no formatting options are set on the
Url
object that is passed in, and you pass in a default_formatting_options, we will copy passed in Url and set that formatting option on it, and return copy.
Returns
Url
- Object of same type as
cls
, which is the class you calledUrl.ensure_url()
on.
Expand source code
@classmethod def ensure_url( cls: Type[T], value: Optional[UrlStr], *, default_formatting_options: UrlFormattingOptions = Default, ) -> T: """Will check to see if the passed in value is a `Url` (depending on the class you call `Url.ensure_url` on). If so, will return value unchanged. This is an optimization and the reason to use this method over just constructing a new `Url` and passing the value into that. If the value is already a Url and it's formatting_options don't need to be set to `default_formatting_options` it will just return it without any extra work. If not then will create a new instance of cls and pass value to `__init__` and return result. ### If you provide `default_formatting_options`: If you pass in a `str` to this method, then any `formatting_options` you passed in will be checked and used if provided. If you pass in a Url, and it has not explicitly set formatting_options, we will make a copy of passed in Url and set `default_formatting_options` on it and return copy. Otherwise, Url will use the usual `DefaultQueryValueListFormat` for the default format. Args: value: You can pass in a UrlStr (which is either a `str` or `Url`), and you will get back a `Url` object from it. If the value is exactly the same as the class you call this class method on, you will get it back unchanged. If not then we create a new object of type `cls` and give value to it as a init parameter. If `value` is not a `Url` or `str`, we will try and convert it to a `str` for you in an attempt to extract a url string from the passed in object. default_formatting_options (UrlFormattingOptions): Formatting options to use IF passed in value is a string. If the passed in value is a Url object, we use whatever it has set on it. If no formatting options are set on the `Url` object that is passed in, and you pass in a default_formatting_options, we will copy passed in Url and set that formatting option on it, and return copy. Returns: Url: Object of same type as `cls`, which is the class you called `Url.ensure_url` on. """ if type(value) is cls and (not default_formatting_options or value.formatting_options): return value if isinstance(value, Url) and value.formatting_options: # Url already has explicitly set formatting-options, keep whatever it has. return cls(value) # Otherwise, we are some other non-Url value (ie: a `str` probably), # try tp convert value to string and any default_formatting_options the user provided. return cls(value, formatting_options=default_formatting_options)
Instance variables
var formatting_options : UrlFormattingOptions
-
This tells Url how to encode/decode multiple values [ie: list] for a particular query name-key.
If
formatting_options
is None, then by defaultDefaultQueryValueListFormat
will be used.Just like other Url attributes, if
formatting_options
is None, it will and you append this Url to another Url,formatting_options
will not change.A query value can be either a str/int, or a list of str/int.
If it is a list of str/int, and if self.formatting_options is True, then the key will have a suffix appended [
__in
by default] and the values will be delimited by a string [,
- comma by default].If self.formatting_options is None, we instead duplicate the key for each value in the list in final url.
Examples for
key='someKey', value=['A', 'B']
:-
self.formatting_options is None:
?someKey=A&someKey=B
-
self.formatting_options is QueryValueListFormat('__in', ','):
?someKey__in=A,B
.
Expand source code
@property def formatting_options(self) -> UrlFormattingOptions: """ This tells Url how to encode/decode multiple values [ie: list] for a particular query name-key. If `formatting_options` is None, then by default `DefaultQueryValueListFormat` will be used. Just like other Url attributes, if `formatting_options` is None, it will and you append this Url to another Url, `formatting_options` will not change. A query value can be either a str/int, or a list of str/int. If it is a list of str/int, and if self.formatting_options is True, then the key will have a suffix appended [`__in` by default] and the values will be delimited by a string [`,` - comma by default]. If self.formatting_options is None, we instead duplicate the key for each value in the list in final url. Examples for `key='someKey', value=['A', 'B']`: - self.formatting_options is None: `?someKey=A&someKey=B` - self.formatting_options is QueryValueListFormat('__in', ','): `?someKey__in=A,B`. """ return self._formatting_options
-
var fragment : Optional[str]
-
Expand source code
@property def fragment(self) -> Optional[str]: return self._fragment
var host : Optional[str]
-
Expand source code
@property def host(self) -> Optional[str]: return self._host
var methods : Optional[tuple]
-
Expand source code
@property def methods(self) -> Optional[tuple]: # Return a read-only copy of the methods. return tuple(self._methods)
var password : Optional[str]
-
Expand source code
@property def password(self) -> Optional[str]: return self._password
var path : Optional[str]
-
Path's can have variable placeholders in them, for more details see Path Formatting Placeholders.
Expand source code
@property def path(self) -> Optional[str]: """Path's can have variable placeholders in them, for more details see [Path Formatting Placeholders](#path-formatting-placeholders). """ return self._path
var port : Optional[int]
-
Expand source code
@property def port(self) -> Optional[int]: return self._port
var query : mappingproxy
-
Returns a live-updating mapping of the url query values as a read-only proxy map.
Expand source code
@property def query(self) -> MappingProxyType: """Returns a live-updating mapping of the url query values as a read-only proxy map.""" # Mapping proxy is a read-only view of the passed in dict. # This will LIVE update the mapping if _query is directly changed! return MappingProxyType(self._query)
var scheme : Optional[str]
-
Expand source code
@property def scheme(self) -> Optional[str]: return self._scheme
var secondary_values : Union[object, dict, Sequence[object]]
-
Returns any SecondaryValues that have been attached to Url. See
self.is_valid(…)
and/orself.url(…)
for more details.Expand source code
@property def secondary_values(self) -> SecondaryValues: """Returns any SecondaryValues that have been attached to Url. See `self.is_valid(...)` and/or `self.url(...)` for more details. """ return self._secondary_values
var singular : Optional[bool]
-
This is a hint to the underlying system that the query will return a singular object instead of potentially many. If this is None, then underlying system may try to inspect results to try and determine if it's many or one/singular.
todo: Implement 'None' value in api.RestClient, right now it treats None as False.
Expand source code
@property def singular(self) -> Optional[bool]: """This is a hint to the underlying system that the query will return a singular object instead of potentially many. If this is None, then underlying system may try to inspect results to try and determine if it's many or one/singular. todo: Implement 'None' value in api.RestClient, right now it treats None as False. """ return self._singular
var username : Optional[str]
-
Expand source code
@property def username(self) -> Optional[str]: return self._username
Methods
def append_methods(self, methods: Iterable[str]) ‑> Url
-
Expand source code
def append_methods(self, methods: Iterable[str]) -> Url: if methods: self._methods.update(methods) return self
def append_path(self, path_component: Optional[str]) ‑> Url
-
Appends path with path_component, making sure to add a slash if needed to separate new component from any current path.
self
is returned, so you can chain this with other method calls.Expand source code
def append_path(self, path_component: Optional[str]) -> Url: """Appends path with path_component, making sure to add a slash if needed to separate new component from any current path. `self` is returned, so you can chain this with other method calls. """ if not path_component: return self path_component = str(path_component) if not path_component: return self # Remove any starting slash if needed, we will be providing our own slash later. if path_component[0] == '/': path_component = path_component[1:] current_path = self._path if not current_path: current_path = '' # If the current path ends in a slash, remove it. if len(current_path) > 0 and current_path[-1] == '/': current_path = current_path[:-1] # Append the new path component with a slash to separate it. self._path = f'{current_path}/{path_component}' self._path_changed() return self
def append_query(self, query: Optional[Query]) ‑> Url
-
Calls
Url.query_add()
for each key/value pair in query. Only keys with the same key-name will be replaced. Otherwise, the query key/value is added to any already existing query key/value pairs.self
is returned, so you can chain this with other method calls.Expand source code
def append_query(self, query: Optional[Query]) -> Url: """Calls `Url.query_add` for each key/value pair in query. Only keys with the same key-name will be replaced. Otherwise, the query key/value is added to any already existing query key/value pairs. `self` is returned, so you can chain this with other method calls. """ if not query: return self for key, value in query.items(): self.query_add(key, value) return self
def append_url(self, url: Optional[UrlStr]) ‑> Url
-
Takes the components of url, and any that look true [ie: are not False or None] are set on myself.
In the case of the path,
append_path
is called instead, so the path provided inurl
is appended to any already existing path.For the query, the
append_query()
is used, and only keys with the same key-name will be replaced. Otherwise, the query key/value is added to any already existing query key/value pairs.For methods,
append_methods
is called, and will add any methods not already present.self
is returned, so you can chain this with other method calls.Expand source code
def append_url(self, url: Optional[UrlStr]) -> Url: """Takes the components of url, and any that look true [ie: are not False or None] are set on myself. In the case of the path, `append_path` is called instead, so the path provided in `url` is appended to any already existing path. For the query, the `append_query()` is used, and only keys with the same key-name will be replaced. Otherwise, the query key/value is added to any already existing query key/value pairs. For methods, `append_methods` is called, and will add any methods not already present. `self` is returned, so you can chain this with other method calls. """ if url is None: return self # Make sure we have a Url, we want to look at it's various components... url = Url.ensure_url(url, default_formatting_options=self.formatting_options) self.append_path(url.path) self.append_query(url._query) self.append_methods(url._methods) for attr_name in _attributes_to_set_when_appending: other_value = getattr(url, attr_name, None) if other_value is not None: setattr(self, attr_name, other_value) return self
def copy(self) ‑> Url
-
We are immutable, so return self without needing to copy.
Expand source code
def copy(self) -> Url: """ We are immutable, so return self without needing to copy. """ return Url(self)
def copy_mutable(self) ‑> Url
-
Returns a copy of self as a Url.
Expand source code
def copy_mutable(self) -> Url: """ Returns a copy of self as a Url. """ return Url(self)
def format(self, *, default_scheme: str = None, secondary_values: SecondaryValues = Default, allow_invalid_url: bool = False) ‑> Optional[str]
-
Returns url as a string, with an optional
default_scheme
.You can get the same value (with all args left at their default values) by simply converting object to a string:
some_url = Url("https://www.some-url.com/") # Ways to convert to string: print(some_url.format()) print(str(some_url)) print(f'Formatted Url: {some_url}')
Output:
https://www.some-url.com/ https://www.some-url.com/ Formatted Url: https://www.some-url.com/
Path's can have variable placeholders in them, for more details see Path Formatting Placeholders
If any placeholder can't find it's value, we return
None
. If you always need a string, pass False to theformat_placeholders
argument.At some point in the future I'll probably support place-holders for query-key/values. The
format_placeholders
argument would also control this too once we get around to supporting that.Args
default_scheme
- If self.scheme is None or blank, use this passed in default scheme.
secondary_values
-
If path needs formatting and there is no valid query value I can use, see if obj has an attribute with a valid value. The attribute name will be same as formatting key name.
If
None
, will check to see if we have any attached self.secondary_values and try to use that if it has something when needed. allow_invalid_url
-
If
True
: Always return astr
with as much as the url formatted as possible. We will never return None in this case.If
False
(default): Always return a fully formatted url, and if we can't do that then returnNone
instead.Important: If you indirectly convert a
Url
object into a string viaf"some url {url_obj}"
, that will pass True toallow_invalid_url
in order to always produce a non-None string, as Python requires this. This is mostly used when logging url objects. Code that always requires a valid url should use this method to explicitly format theUrl
object into a fully-formatted url string!
Returns
Normal string as a fully constructed url.
If
None
is returned, the Url could not be formatted (seeUrl.is_valid()
). There is a missing or invalid value for one of the required formatting key-names.See Url class doc section Path Formatting Placeholders. for more details.
Expand source code
def format( self, *, default_scheme: str = None, secondary_values: SecondaryValues = Default, allow_invalid_url: bool = False, ) -> Optional[str]: """ Returns url as a string, with an optional `default_scheme`. You can get the same value (with all args left at their default values) by simply converting object to a string: ```python some_url = Url("https://www.some-url.com/") # Ways to convert to string: print(some_url.format()) print(str(some_url)) print(f'Formatted Url: {some_url}') ``` Output: ``` https://www.some-url.com/ https://www.some-url.com/ Formatted Url: https://www.some-url.com/ ``` Path's can have variable placeholders in them, for more details see [Path Formatting Placeholders](#path-formatting-placeholders) If any placeholder can't find it's value, we return `None`. If you always need a string, pass False to the `format_placeholders` argument. At some point in the future I'll probably support place-holders for query-key/values. The `format_placeholders` argument would also control this too once we get around to supporting that. Args: default_scheme: If self.scheme is None or blank, use this passed in default scheme. secondary_values: If path needs formatting and there is no valid query value I can use, see if obj has an attribute with a valid value. The attribute name will be same as formatting key name. If `None`, will check to see if we have any attached self.secondary_values and try to use that if it has something when needed. allow_invalid_url: If `True`: Always return a `str` with as much as the url formatted as possible. We will never return None in this case. If `False` (default): Always return a fully formatted url, and if we can't do that then return `None` instead. .. important:: If you indirectly convert a `Url` object into a string via `f"some url {url_obj}"`, that will pass True to `allow_invalid_url` in order to always produce a non-None string, as Python requires this. This is mostly used when logging url objects. Code that always requires a valid url should use this method to explicitly format the `Url` object into a fully-formatted url string! Returns: Normal string as a fully constructed url. If `None` is returned, the Url could not be formatted (see `Url.is_valid`). There is a missing or invalid value for one of the required formatting key-names. See Url class doc section [Path Formatting Placeholders](#path-formatting-placeholders). for more details. """ used_query_keys: List[str] = [] path = self._formatted_path( secondary_values=secondary_values, query_keys_used=used_query_keys ) # If we can't format the path due to finding all placeholders, # then we return None to indicate we can't format the url. if path is None: if not allow_invalid_url: return None # Use unformatted / raw url path as we can't format it and we have allow_invalid_url. path = self.path netloc = '' if self._host is not None: netloc = self._host userpass = None if self._username is not None: userpass = urlparser.quote(self._username) if self._password is not None: userpass = f"{userpass}:{urlparser.quote(self._password)}" if userpass is not None: netloc = f"{userpass}@{netloc}" if self._port is not None: netloc = f"{netloc}:{self._port}" scheme = self._scheme or default_scheme or '' fragment = self._fragment or '' query_dict = self._query if used_query_keys: # Filter out any keys that we should ignore. # Filters dict and makes a copy at the same time, exactly what we need! query_dict = {k: query_dict[k] for k in query_dict if k not in used_query_keys} query = self._format_query_into_string(query_dict) return urlparser.urlunparse((scheme, netloc, path or '', '', query, fragment))
def is_valid(self, secondary_values: SecondaryValues = Default, attach_values: bool = False) ‑> bool
-
A more efficient way to determine if we can produce a valid url string. The efficiency comes from not needing to actually format and produce the url string.
Return True if self.url() would return a valid url, else False.
The Url version of this method allows you to 'attach' the secondary values to the url. If so, will use the attached values when calling self.url(…) and passing no secondary values into that method.
Args
secondary_values
- If query does not have a needed value to satisfy a formatting placeholder, we will look at values in here. It can be a object, dict or a sequence of objects. If the object(s) or dict has the needed value, then we will use that to satisfy the url placeholder.
attach_values
-
If
True
, will 'attach' the secondary values IF url is valid with them. Attaching means that they are the default secondary values used when generating the Url or when calling is_valid(…) in the future.If attach_values is a Class/Object, it will directly set this value into the new copy. We will do a shallow-copy IF the value is a dict/list.
Returns
Expand source code
def is_valid( self, secondary_values: SecondaryValues = Default, attach_values: bool = False ) -> bool: """ A more efficient way to determine if we can produce a valid url string. The efficiency comes from not needing to actually format and produce the url string. Return True if self.url() would return a valid url, else False. The Url version of this method allows you to 'attach' the secondary values to the url. If so, will use the attached values when calling self.url(...) and passing no secondary values into that method. Args: secondary_values: If query does not have a needed value to satisfy a formatting placeholder, we will look at values in here. It can be a object, dict or a sequence of objects. If the object(s) or dict has the needed value, then we will use that to satisfy the url placeholder. attach_values: If `True`, will 'attach' the secondary values **IF** url is valid with them. Attaching means that they are the default secondary values used when generating the Url or when calling is_valid(...) in the future. If attach_values is a Class/Object, it will directly set this value into the new copy. We will do a shallow-copy IF the value is a dict/list. Returns: True: `Url` is valid and you can call `Url.url` without a problem (with same secondary_values if you did not attach them). False: `Url` is invalid, it needs more formatting params or there is some other issue. """ format_map = self._formatted_map(secondary_values=secondary_values) if format_map and attach_values: # Do a shallow copy if it's a list/dict, like we document we do. # We don't make a copy for anything else (like a normal object). if isinstance(secondary_values, (list, dict)): secondary_values = copy(secondary_values) # Attach these as the default secondary_values when formatting self.url() self._secondary_values = secondary_values return format_map is not None
def methods_add(self, method: str) ‑> Url
-
Expand source code
def methods_add(self, method: str) -> Url: if method: self._methods.add(method) return self
def methods_contain(self, method: str) ‑> bool
-
Returns True if method is one of my methods, or if I have not assigned methods. Otherwise, returns False.
It's faster to use this method then to get all methods and look yourself since I use a set internally and can more quickly lookup things in the set.
Expand source code
def methods_contain(self, method: str) -> bool: """Returns True if method is one of my methods, or if I have not assigned methods. Otherwise, returns False. It's faster to use this method then to get all methods and look yourself since I use a set internally and can more quickly lookup things in the set. """ if not self._methods: return True return method in self._methods
def methods_have_one_in(self, methods: Set[str]) ‑> bool
-
Expand source code
def methods_have_one_in(self, methods: Set[str]) -> bool: return not self._methods.isdisjoint(methods or set())
def methods_remove(self, method: str) ‑> Url
-
Expand source code
def methods_remove(self, method: str) -> Url: if method: self._methods.discard(method) return self
def query_add(self, key: str = None, value: QueryValue = None) ‑> Url
-
Use to set a key in query easily. This will entirely replace query with name
key
if the value is not None. If value is None, nothing will happen/change. If you want to remove a query item, use 'self.query_remove(…)' instead.A query value can be either a str/int, or a list of str/int. If it is a list, then see self.formatting_options for details on what happens.
A shallow copy will be made of value, ie:
copy(value)
before it's added to url.self
is returned, so you can chain this with other method calls.Expand source code
def query_add(self, key: str = None, value: QueryValue = None) -> Url: """Use to set a key in query easily. This will entirely replace query with name `key` if the value is not None. If value is None, nothing will happen/change. If you want to remove a query item, use 'self.query_remove(...)' instead. A query value can be either a str/int, or a list of str/int. If it is a list, then see self.formatting_options for details on what happens. A shallow copy will be made of value, ie: `copy(value)` before it's added to url. `self` is returned, so you can chain this with other method calls. """ if value is None: return self if self._query is None: self._query = {} self._query[key] = copy(value) return self
def query_id_if_singular(self)
-
Expand source code
def query_id_if_singular(self): val = self._query.get('id') if val is None: return None if isinstance(val, (str, int)): return val new_val = list(map(str, val)) if len(new_val) == 1: return new_val[0] return None
def query_remove(self, key) ‑> Url
-
Remove value for key in query, if key does not exist nothing happens.
self
is returned, so you can chain this with other method calls.Expand source code
def query_remove(self, key) -> Url: """Remove value for key in query, if key does not exist nothing happens. `self` is returned, so you can chain this with other method calls. """ self._query.pop(key, None) return self
def query_value(self, key: str) ‑> Union[str, int, datetime.date, xurls.url._FormattedQueryValue, ForwardRef(None), Iterable[Union[str, int, datetime.date, xurls.url._FormattedQueryValue]]]
-
Returns the value in query assigned to key, if key not found returns
None
.Expand source code
def query_value(self, key: str) -> Optional[QueryValue]: """ Returns the value in query assigned to key, if key not found returns `None`. """ return self._query.get(key, None)
def set_formatting_options(self, value: Optional[UrlFormattingOptions]) ‑> Url
-
Does
self.formatting_options = value
for you while returningself
.self
is returned, so you can easily chain this with other method calls if you wish.Expand source code
def set_formatting_options(self, value: Optional[UrlFormattingOptions]) -> Url: """Does `self.formatting_options = value` for you while returning `self`. `self` is returned, so you can easily chain this with other method calls if you wish. """ self.formatting_options = value return self
def set_methods(self, methods: Union[Iterable[str], str], *args)
-
Sets methods and returns self, so you can chain it with other mods. You can pass in an iterable or a direct string, for any number of arguments. We will iterate each argument if it's not a string to combine all of them together in one list, which will be set on self.methods.
self
is returned, so you can chain this with other method calls.Expand source code
def set_methods(self, methods: Union[Iterable[str], str], *args): """Sets methods and returns self, so you can chain it with other mods. You can pass in an iterable or a direct string, for any number of arguments. We will iterate each argument if it's not a string to combine all of them together in one list, which will be set on self.methods. `self` is returned, so you can chain this with other method calls. """ self._set_methods(xloop(methods, args)) return self
def set_singular(self, value: Optional[bool]) ‑> Url
-
Sets singular to value.
self
is returned, so you can chain this with other method calls.Expand source code
def set_singular(self, value: Optional[bool]) -> Url: """Sets singular to value. `self` is returned, so you can chain this with other method calls. """ self.singular = value return self
def url(self, *, default_scheme: str = None, secondary_values: SecondaryValues = Default, allow_invalid_url: bool = False)
-
Expand source code
def url( self, *, default_scheme: str = None, secondary_values: SecondaryValues = Default, allow_invalid_url: bool = False, ): warn( "Url.url is deprecated, instead use `Url.format` " "or directly convert url object to a str.", DeprecationWarning, 2 ) return self.format( default_scheme=default_scheme, secondary_values=secondary_values, allow_invalid_url=allow_invalid_url )
-
class UrlFormattingOptions (list_key_suffix: str = '__in', list_value_delimiter: str = ',')
-
UrlFormattingOptions(list_key_suffix: 'str' = '__in', list_value_delimiter: 'str' = ',')
Expand source code
@dataclass class UrlFormattingOptions: # TODO: Allow for a configurable formatting callback function/object. list_key_suffix: str = '__in' """ What to strip/append to query key-name if the value is a list. When parsing a Url, if the key name does not end with this, we won't try to split the value by the `list_value_delimiter`, the value will be left unchanged. When parsing a Url, the list_key_suffix will be striped from the key-name. When constructing the Url into a string, list_key_suffix will be added to a key-name if the value is a list. Default is '__in', which is the standard way Django Rest Framework's filters work. """ list_key_suffixes_to_keep = [ '__fields__include', '__fields__only', '__fields__exclude' ] """ Suffixes listed here can implicitly take a delimited list of values without using the generalized `UrlFormattingOptions.list_key_suffix` (see above). The `UrlFormattingOptions.list_key_suffix` won't be used if a key ends with one of these keys, but instead the list will be directly formatted/parsed directly out of the value without modifying the key. """ list_value_delimiter: str = ',' """ This tells the Url the character to split query value by. If there is no delimiter in value, then a list with a single value is the result. Keep in mind that list won't be parsed from a queries value if the queries key-name does not end with `list_key_suffix`. The `list_key_suffix` is what controls if the query value is a list or not. Default is a comma `,`. """
Class variables
var list_key_suffix : str
-
What to strip/append to query key-name if the value is a list. When parsing a Url, if the key name does not end with this, we won't try to split the value by the
list_value_delimiter
, the value will be left unchanged.When parsing a Url, the list_key_suffix will be striped from the key-name. When constructing the Url into a string, list_key_suffix will be added to a key-name if the value is a list.
Default is '__in', which is the standard way Django Rest Framework's filters work.
var list_key_suffixes_to_keep
-
Suffixes listed here can implicitly take a delimited list of values without using the generalized
UrlFormattingOptions.list_key_suffix
(see above).The
UrlFormattingOptions.list_key_suffix
won't be used if a key ends with one of these keys, but instead the list will be directly formatted/parsed directly out of the value without modifying the key. var list_value_delimiter : str
-
This tells the Url the character to split query value by.
If there is no delimiter in value, then a list with a single value is the result. Keep in mind that list won't be parsed from a queries value if the queries key-name does not end with
list_key_suffix
. Thelist_key_suffix
is what controls if the query value is a list or not.Default is a comma
,
.