linq-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linq/__init__.py +102 -0
- linq/_base_client.py +2149 -0
- linq/_client.py +2479 -0
- linq/_compat.py +226 -0
- linq/_constants.py +14 -0
- linq/_exceptions.py +108 -0
- linq/_files.py +123 -0
- linq/_models.py +878 -0
- linq/_qs.py +153 -0
- linq/_resource.py +43 -0
- linq/_response.py +833 -0
- linq/_streaming.py +338 -0
- linq/_types.py +271 -0
- linq/_utils/__init__.py +65 -0
- linq/_utils/_compat.py +45 -0
- linq/_utils/_datetime_parse.py +136 -0
- linq/_utils/_json.py +35 -0
- linq/_utils/_logs.py +25 -0
- linq/_utils/_path.py +127 -0
- linq/_utils/_proxy.py +65 -0
- linq/_utils/_reflection.py +42 -0
- linq/_utils/_resources_proxy.py +24 -0
- linq/_utils/_streams.py +12 -0
- linq/_utils/_sync.py +58 -0
- linq/_utils/_transform.py +457 -0
- linq/_utils/_typing.py +156 -0
- linq/_utils/_utils.py +421 -0
- linq/_version.py +4 -0
- linq/lib/.keep +4 -0
- linq/pagination.py +95 -0
- linq/py.typed +0 -0
- linq/resources/__init__.py +134 -0
- linq/resources/attachments.py +589 -0
- linq/resources/capability.py +297 -0
- linq/resources/chats/__init__.py +61 -0
- linq/resources/chats/chats.py +1492 -0
- linq/resources/chats/messages.py +416 -0
- linq/resources/chats/participants.py +322 -0
- linq/resources/chats/typing.py +299 -0
- linq/resources/contact_card.py +472 -0
- linq/resources/messages.py +686 -0
- linq/resources/phone_numbers.py +163 -0
- linq/resources/phonenumbers.py +165 -0
- linq/resources/webhook_events.py +319 -0
- linq/resources/webhook_subscriptions.py +776 -0
- linq/resources/webhooks.py +34 -0
- linq/types/__init__.py +90 -0
- linq/types/attachment_create_params.py +42 -0
- linq/types/attachment_create_response.py +44 -0
- linq/types/attachment_retrieve_response.py +55 -0
- linq/types/capability_check_RCS_params.py +20 -0
- linq/types/capability_check_i_message_params.py +20 -0
- linq/types/chat.py +44 -0
- linq/types/chat_create_params.py +33 -0
- linq/types/chat_create_response.py +44 -0
- linq/types/chat_created_webhook_event.py +87 -0
- linq/types/chat_group_icon_update_failed_webhook_event.py +65 -0
- linq/types/chat_group_icon_updated_webhook_event.py +66 -0
- linq/types/chat_group_name_update_failed_webhook_event.py +65 -0
- linq/types/chat_group_name_updated_webhook_event.py +66 -0
- linq/types/chat_leave_chat_response.py +15 -0
- linq/types/chat_list_chats_params.py +36 -0
- linq/types/chat_send_voicememo_params.py +23 -0
- linq/types/chat_send_voicememo_response.py +79 -0
- linq/types/chat_typing_indicator_started_webhook_event.py +52 -0
- linq/types/chat_typing_indicator_stopped_webhook_event.py +52 -0
- linq/types/chat_update_params.py +15 -0
- linq/types/chat_update_response.py +13 -0
- linq/types/chats/__init__.py +12 -0
- linq/types/chats/message_list_params.py +15 -0
- linq/types/chats/message_send_params.py +18 -0
- linq/types/chats/message_send_response.py +16 -0
- linq/types/chats/participant_add_params.py +12 -0
- linq/types/chats/participant_add_response.py +15 -0
- linq/types/chats/participant_remove_params.py +12 -0
- linq/types/chats/participant_remove_response.py +15 -0
- linq/types/chats/sent_message.py +69 -0
- linq/types/contact_card_create_params.py +24 -0
- linq/types/contact_card_retrieve_params.py +15 -0
- linq/types/contact_card_retrieve_response.py +23 -0
- linq/types/contact_card_update_params.py +21 -0
- linq/types/events_webhook_event.py +50 -0
- linq/types/handle_check_response.py +13 -0
- linq/types/link_part_param.py +22 -0
- linq/types/media_part_param.py +54 -0
- linq/types/message.py +87 -0
- linq/types/message_add_reaction_params.py +32 -0
- linq/types/message_add_reaction_response.py +15 -0
- linq/types/message_content_param.py +82 -0
- linq/types/message_delivered_webhook_event.py +65 -0
- linq/types/message_edited_webhook_event.py +100 -0
- linq/types/message_effect.py +23 -0
- linq/types/message_effect_param.py +22 -0
- linq/types/message_event_v2.py +116 -0
- linq/types/message_failed_webhook_event.py +72 -0
- linq/types/message_list_messages_thread_params.py +18 -0
- linq/types/message_read_webhook_event.py +65 -0
- linq/types/message_received_webhook_event.py +65 -0
- linq/types/message_sent_webhook_event.py +65 -0
- linq/types/message_update_params.py +15 -0
- linq/types/participant_added_webhook_event.py +66 -0
- linq/types/participant_removed_webhook_event.py +66 -0
- linq/types/phone_number_list_response.py +20 -0
- linq/types/phone_number_status_updated_webhook_event.py +82 -0
- linq/types/phonenumber_list_response.py +39 -0
- linq/types/reaction_added_webhook_event.py +46 -0
- linq/types/reaction_event_base.py +85 -0
- linq/types/reaction_removed_webhook_event.py +46 -0
- linq/types/reply_to.py +21 -0
- linq/types/reply_to_param.py +21 -0
- linq/types/schemas_media_part_response.py +29 -0
- linq/types/schemas_message_effect.py +18 -0
- linq/types/schemas_text_part_response.py +22 -0
- linq/types/set_contact_card.py +24 -0
- linq/types/shared/__init__.py +9 -0
- linq/types/shared/chat_handle.py +33 -0
- linq/types/shared/media_part_response.py +34 -0
- linq/types/shared/reaction.py +56 -0
- linq/types/shared/reaction_type.py +7 -0
- linq/types/shared/service_type.py +7 -0
- linq/types/shared/text_decoration.py +23 -0
- linq/types/shared/text_part_response.py +26 -0
- linq/types/shared_params/__init__.py +5 -0
- linq/types/shared_params/reaction_type.py +9 -0
- linq/types/shared_params/service_type.py +9 -0
- linq/types/shared_params/text_decoration.py +23 -0
- linq/types/supported_content_type.py +60 -0
- linq/types/text_part_param.py +44 -0
- linq/types/webhook_event_list_response.py +17 -0
- linq/types/webhook_event_type.py +33 -0
- linq/types/webhook_subscription.py +35 -0
- linq/types/webhook_subscription_create_params.py +27 -0
- linq/types/webhook_subscription_create_response.py +46 -0
- linq/types/webhook_subscription_list_response.py +13 -0
- linq/types/webhook_subscription_update_params.py +30 -0
- linq_python-0.1.0.dist-info/METADATA +572 -0
- linq_python-0.1.0.dist-info/RECORD +139 -0
- linq_python-0.1.0.dist-info/WHEEL +4 -0
- linq_python-0.1.0.dist-info/licenses/LICENSE +201 -0
linq/_utils/_compat.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typing_extensions
|
|
5
|
+
from typing import Any, Type, Union, Literal, Optional
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from typing_extensions import get_args as _get_args, get_origin as _get_origin
|
|
8
|
+
|
|
9
|
+
from .._types import StrBytesIntFloat
|
|
10
|
+
from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime
|
|
11
|
+
|
|
12
|
+
_LITERAL_TYPES = {Literal, typing_extensions.Literal}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_args(tp: type[Any]) -> tuple[Any, ...]:
|
|
16
|
+
return _get_args(tp)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_origin(tp: type[Any]) -> type[Any] | None:
|
|
20
|
+
return _get_origin(tp)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_union(tp: Optional[Type[Any]]) -> bool:
|
|
24
|
+
if sys.version_info < (3, 10):
|
|
25
|
+
return tp is Union # type: ignore[comparison-overlap]
|
|
26
|
+
else:
|
|
27
|
+
import types
|
|
28
|
+
|
|
29
|
+
return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_typeddict(tp: Type[Any]) -> bool:
|
|
33
|
+
return typing_extensions.is_typeddict(tp)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_literal_type(tp: Type[Any]) -> bool:
|
|
37
|
+
return get_origin(tp) in _LITERAL_TYPES
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
|
|
41
|
+
return _parse_date(value)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
|
|
45
|
+
return _parse_datetime(value)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py
|
|
3
|
+
without the Pydantic v1 specific errors.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, Union, Optional
|
|
10
|
+
from datetime import date, datetime, timezone, timedelta
|
|
11
|
+
|
|
12
|
+
from .._types import StrBytesIntFloat
|
|
13
|
+
|
|
14
|
+
date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
|
15
|
+
time_expr = (
|
|
16
|
+
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
|
17
|
+
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
|
|
18
|
+
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
date_re = re.compile(f"{date_expr}$")
|
|
22
|
+
datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
EPOCH = datetime(1970, 1, 1)
|
|
26
|
+
# if greater than this, the number is in ms, if less than or equal it's in seconds
|
|
27
|
+
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
|
|
28
|
+
MS_WATERSHED = int(2e10)
|
|
29
|
+
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
|
|
30
|
+
MAX_NUMBER = int(3e20)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
|
|
34
|
+
if isinstance(value, (int, float)):
|
|
35
|
+
return value
|
|
36
|
+
try:
|
|
37
|
+
return float(value)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return None
|
|
40
|
+
except TypeError:
|
|
41
|
+
raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _from_unix_seconds(seconds: Union[int, float]) -> datetime:
|
|
45
|
+
if seconds > MAX_NUMBER:
|
|
46
|
+
return datetime.max
|
|
47
|
+
elif seconds < -MAX_NUMBER:
|
|
48
|
+
return datetime.min
|
|
49
|
+
|
|
50
|
+
while abs(seconds) > MS_WATERSHED:
|
|
51
|
+
seconds /= 1000
|
|
52
|
+
dt = EPOCH + timedelta(seconds=seconds)
|
|
53
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]:
|
|
57
|
+
if value == "Z":
|
|
58
|
+
return timezone.utc
|
|
59
|
+
elif value is not None:
|
|
60
|
+
offset_mins = int(value[-2:]) if len(value) > 3 else 0
|
|
61
|
+
offset = 60 * int(value[1:3]) + offset_mins
|
|
62
|
+
if value[0] == "-":
|
|
63
|
+
offset = -offset
|
|
64
|
+
return timezone(timedelta(minutes=offset))
|
|
65
|
+
else:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
|
|
70
|
+
"""
|
|
71
|
+
Parse a datetime/int/float/string and return a datetime.datetime.
|
|
72
|
+
|
|
73
|
+
This function supports time zone offsets. When the input contains one,
|
|
74
|
+
the output uses a timezone with a fixed offset from UTC.
|
|
75
|
+
|
|
76
|
+
Raise ValueError if the input is well formatted but not a valid datetime.
|
|
77
|
+
Raise ValueError if the input isn't well formatted.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(value, datetime):
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
number = _get_numeric(value, "datetime")
|
|
83
|
+
if number is not None:
|
|
84
|
+
return _from_unix_seconds(number)
|
|
85
|
+
|
|
86
|
+
if isinstance(value, bytes):
|
|
87
|
+
value = value.decode()
|
|
88
|
+
|
|
89
|
+
assert not isinstance(value, (float, int))
|
|
90
|
+
|
|
91
|
+
match = datetime_re.match(value)
|
|
92
|
+
if match is None:
|
|
93
|
+
raise ValueError("invalid datetime format")
|
|
94
|
+
|
|
95
|
+
kw = match.groupdict()
|
|
96
|
+
if kw["microsecond"]:
|
|
97
|
+
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
|
|
98
|
+
|
|
99
|
+
tzinfo = _parse_timezone(kw.pop("tzinfo"))
|
|
100
|
+
kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
|
|
101
|
+
kw_["tzinfo"] = tzinfo
|
|
102
|
+
|
|
103
|
+
return datetime(**kw_) # type: ignore
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
|
|
107
|
+
"""
|
|
108
|
+
Parse a date/int/float/string and return a datetime.date.
|
|
109
|
+
|
|
110
|
+
Raise ValueError if the input is well formatted but not a valid date.
|
|
111
|
+
Raise ValueError if the input isn't well formatted.
|
|
112
|
+
"""
|
|
113
|
+
if isinstance(value, date):
|
|
114
|
+
if isinstance(value, datetime):
|
|
115
|
+
return value.date()
|
|
116
|
+
else:
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
number = _get_numeric(value, "date")
|
|
120
|
+
if number is not None:
|
|
121
|
+
return _from_unix_seconds(number).date()
|
|
122
|
+
|
|
123
|
+
if isinstance(value, bytes):
|
|
124
|
+
value = value.decode()
|
|
125
|
+
|
|
126
|
+
assert not isinstance(value, (float, int))
|
|
127
|
+
match = date_re.match(value)
|
|
128
|
+
if match is None:
|
|
129
|
+
raise ValueError("invalid date format")
|
|
130
|
+
|
|
131
|
+
kw = {k: int(v) for k, v in match.groupdict().items()}
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
return date(**kw)
|
|
135
|
+
except ValueError:
|
|
136
|
+
raise ValueError("invalid date format") from None
|
linq/_utils/_json.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
import pydantic
|
|
7
|
+
|
|
8
|
+
from .._compat import model_dump
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def openapi_dumps(obj: Any) -> bytes:
|
|
12
|
+
"""
|
|
13
|
+
Serialize an object to UTF-8 encoded JSON bytes.
|
|
14
|
+
|
|
15
|
+
Extends the standard json.dumps with support for additional types
|
|
16
|
+
commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
|
|
17
|
+
"""
|
|
18
|
+
return json.dumps(
|
|
19
|
+
obj,
|
|
20
|
+
cls=_CustomEncoder,
|
|
21
|
+
# Uses the same defaults as httpx's JSON serialization
|
|
22
|
+
ensure_ascii=False,
|
|
23
|
+
separators=(",", ":"),
|
|
24
|
+
allow_nan=False,
|
|
25
|
+
).encode()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _CustomEncoder(json.JSONEncoder):
|
|
29
|
+
@override
|
|
30
|
+
def default(self, o: Any) -> Any:
|
|
31
|
+
if isinstance(o, datetime):
|
|
32
|
+
return o.isoformat()
|
|
33
|
+
if isinstance(o, pydantic.BaseModel):
|
|
34
|
+
return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
|
|
35
|
+
return super().default(o)
|
linq/_utils/_logs.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
logger: logging.Logger = logging.getLogger("linq")
|
|
5
|
+
httpx_logger: logging.Logger = logging.getLogger("httpx")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _basic_config() -> None:
|
|
9
|
+
# e.g. [2023-10-05 14:12:26 - linq._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK"
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
|
|
12
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def setup_logging() -> None:
|
|
17
|
+
env = os.environ.get("LINQ_API_V3_LOG")
|
|
18
|
+
if env == "debug":
|
|
19
|
+
_basic_config()
|
|
20
|
+
logger.setLevel(logging.DEBUG)
|
|
21
|
+
httpx_logger.setLevel(logging.DEBUG)
|
|
22
|
+
elif env == "info":
|
|
23
|
+
_basic_config()
|
|
24
|
+
logger.setLevel(logging.INFO)
|
|
25
|
+
httpx_logger.setLevel(logging.INFO)
|
linq/_utils/_path.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Mapping,
|
|
7
|
+
Callable,
|
|
8
|
+
)
|
|
9
|
+
from urllib.parse import quote
|
|
10
|
+
|
|
11
|
+
# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
|
|
12
|
+
_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
|
|
13
|
+
|
|
14
|
+
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _quote_path_segment_part(value: str) -> str:
|
|
18
|
+
"""Percent-encode `value` for use in a URI path segment.
|
|
19
|
+
|
|
20
|
+
Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
|
|
21
|
+
https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
|
|
22
|
+
"""
|
|
23
|
+
# quote() already treats unreserved characters (letters, digits, and -._~)
|
|
24
|
+
# as safe, so we only need to add sub-delims, ':', and '@'.
|
|
25
|
+
# Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
|
|
26
|
+
return quote(value, safe="!$&'()*+,;=:@")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _quote_query_part(value: str) -> str:
|
|
30
|
+
"""Percent-encode `value` for use in a URI query string.
|
|
31
|
+
|
|
32
|
+
Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
|
|
33
|
+
https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
|
|
34
|
+
"""
|
|
35
|
+
return quote(value, safe="!$'()*+,;:@/?")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _quote_fragment_part(value: str) -> str:
|
|
39
|
+
"""Percent-encode `value` for use in a URI fragment.
|
|
40
|
+
|
|
41
|
+
Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
|
|
42
|
+
https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
|
|
43
|
+
"""
|
|
44
|
+
return quote(value, safe="!$&'()*+,;=:@/?")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _interpolate(
|
|
48
|
+
template: str,
|
|
49
|
+
values: Mapping[str, Any],
|
|
50
|
+
quoter: Callable[[str], str],
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Replace {name} placeholders in `template`, quoting each value with `quoter`.
|
|
53
|
+
|
|
54
|
+
Placeholder names are looked up in `values`.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
KeyError: If a placeholder is not found in `values`.
|
|
58
|
+
"""
|
|
59
|
+
# re.split with a capturing group returns alternating
|
|
60
|
+
# [text, name, text, name, ..., text] elements.
|
|
61
|
+
parts = _PLACEHOLDER_RE.split(template)
|
|
62
|
+
|
|
63
|
+
for i in range(1, len(parts), 2):
|
|
64
|
+
name = parts[i]
|
|
65
|
+
if name not in values:
|
|
66
|
+
raise KeyError(f"a value for placeholder {{{name}}} was not provided")
|
|
67
|
+
val = values[name]
|
|
68
|
+
if val is None:
|
|
69
|
+
parts[i] = "null"
|
|
70
|
+
elif isinstance(val, bool):
|
|
71
|
+
parts[i] = "true" if val else "false"
|
|
72
|
+
else:
|
|
73
|
+
parts[i] = quoter(str(values[name]))
|
|
74
|
+
|
|
75
|
+
return "".join(parts)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def path_template(template: str, /, **kwargs: Any) -> str:
|
|
79
|
+
"""Interpolate {name} placeholders in `template` from keyword arguments.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
template: The template string containing {name} placeholders.
|
|
83
|
+
**kwargs: Keyword arguments to interpolate into the template.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The template with placeholders interpolated and percent-encoded.
|
|
87
|
+
|
|
88
|
+
Safe characters for percent-encoding are dependent on the URI component.
|
|
89
|
+
Placeholders in path and fragment portions are percent-encoded where the `segment`
|
|
90
|
+
and `fragment` sets from RFC 3986 respectively are considered safe.
|
|
91
|
+
Placeholders in the query portion are percent-encoded where the `query` set from
|
|
92
|
+
RFC 3986 §3.3 is considered safe except for = and & characters.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
KeyError: If a placeholder is not found in `kwargs`.
|
|
96
|
+
ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
|
|
97
|
+
"""
|
|
98
|
+
# Split the template into path, query, and fragment portions.
|
|
99
|
+
fragment_template: str | None = None
|
|
100
|
+
query_template: str | None = None
|
|
101
|
+
|
|
102
|
+
rest = template
|
|
103
|
+
if "#" in rest:
|
|
104
|
+
rest, fragment_template = rest.split("#", 1)
|
|
105
|
+
if "?" in rest:
|
|
106
|
+
rest, query_template = rest.split("?", 1)
|
|
107
|
+
path_template = rest
|
|
108
|
+
|
|
109
|
+
# Interpolate each portion with the appropriate quoting rules.
|
|
110
|
+
path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
|
|
111
|
+
|
|
112
|
+
# Reject dot-segments (. and ..) in the final assembled path. The check
|
|
113
|
+
# runs after interpolation so that adjacent placeholders or a mix of static
|
|
114
|
+
# text and placeholders that together form a dot-segment are caught.
|
|
115
|
+
# Also reject percent-encoded dot-segments to protect against incorrectly
|
|
116
|
+
# implemented normalization in servers/proxies.
|
|
117
|
+
for segment in path_result.split("/"):
|
|
118
|
+
if _DOT_SEGMENT_RE.match(segment):
|
|
119
|
+
raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
|
|
120
|
+
|
|
121
|
+
result = path_result
|
|
122
|
+
if query_template is not None:
|
|
123
|
+
result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
|
|
124
|
+
if fragment_template is not None:
|
|
125
|
+
result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
|
|
126
|
+
|
|
127
|
+
return result
|
linq/_utils/_proxy.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Generic, TypeVar, Iterable, cast
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LazyProxy(Generic[T], ABC):
|
|
11
|
+
"""Implements data methods to pretend that an instance is another instance.
|
|
12
|
+
|
|
13
|
+
This includes forwarding attribute access and other methods.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Note: we have to special case proxies that themselves return proxies
|
|
17
|
+
# to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz`
|
|
18
|
+
|
|
19
|
+
def __getattr__(self, attr: str) -> object:
|
|
20
|
+
proxied = self.__get_proxied__()
|
|
21
|
+
if isinstance(proxied, LazyProxy):
|
|
22
|
+
return proxied # pyright: ignore
|
|
23
|
+
return getattr(proxied, attr)
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
proxied = self.__get_proxied__()
|
|
28
|
+
if isinstance(proxied, LazyProxy):
|
|
29
|
+
return proxied.__class__.__name__
|
|
30
|
+
return repr(self.__get_proxied__())
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
proxied = self.__get_proxied__()
|
|
35
|
+
if isinstance(proxied, LazyProxy):
|
|
36
|
+
return proxied.__class__.__name__
|
|
37
|
+
return str(proxied)
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
def __dir__(self) -> Iterable[str]:
|
|
41
|
+
proxied = self.__get_proxied__()
|
|
42
|
+
if isinstance(proxied, LazyProxy):
|
|
43
|
+
return []
|
|
44
|
+
return proxied.__dir__()
|
|
45
|
+
|
|
46
|
+
@property # type: ignore
|
|
47
|
+
@override
|
|
48
|
+
def __class__(self) -> type: # pyright: ignore
|
|
49
|
+
try:
|
|
50
|
+
proxied = self.__get_proxied__()
|
|
51
|
+
except Exception:
|
|
52
|
+
return type(self)
|
|
53
|
+
if issubclass(type(proxied), LazyProxy):
|
|
54
|
+
return type(proxied)
|
|
55
|
+
return proxied.__class__
|
|
56
|
+
|
|
57
|
+
def __get_proxied__(self) -> T:
|
|
58
|
+
return self.__load__()
|
|
59
|
+
|
|
60
|
+
def __as_proxied__(self) -> T:
|
|
61
|
+
"""Helper method that returns the current proxy, typed as the loaded object"""
|
|
62
|
+
return cast(T, self)
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def __load__(self) -> T: ...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
|
|
8
|
+
"""Returns whether or not the given function has a specific parameter"""
|
|
9
|
+
sig = inspect.signature(func)
|
|
10
|
+
return arg_name in sig.parameters
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def assert_signatures_in_sync(
|
|
14
|
+
source_func: Callable[..., Any],
|
|
15
|
+
check_func: Callable[..., Any],
|
|
16
|
+
*,
|
|
17
|
+
exclude_params: set[str] = set(),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Ensure that the signature of the second function matches the first."""
|
|
20
|
+
|
|
21
|
+
check_sig = inspect.signature(check_func)
|
|
22
|
+
source_sig = inspect.signature(source_func)
|
|
23
|
+
|
|
24
|
+
errors: list[str] = []
|
|
25
|
+
|
|
26
|
+
for name, source_param in source_sig.parameters.items():
|
|
27
|
+
if name in exclude_params:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
custom_param = check_sig.parameters.get(name)
|
|
31
|
+
if not custom_param:
|
|
32
|
+
errors.append(f"the `{name}` param is missing")
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if custom_param.annotation != source_param.annotation:
|
|
36
|
+
errors.append(
|
|
37
|
+
f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}"
|
|
38
|
+
)
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if errors:
|
|
42
|
+
raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
from ._proxy import LazyProxy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResourcesProxy(LazyProxy[Any]):
|
|
10
|
+
"""A proxy for the `linq.resources` module.
|
|
11
|
+
|
|
12
|
+
This is used so that we can lazily import `linq.resources` only when
|
|
13
|
+
needed *and* so that users can just import `linq` and reference `linq.resources`
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
def __load__(self) -> Any:
|
|
18
|
+
import importlib
|
|
19
|
+
|
|
20
|
+
mod = importlib.import_module("linq.resources")
|
|
21
|
+
return mod
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
resources = ResourcesProxy().__as_proxied__()
|
linq/_utils/_streams.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from typing_extensions import Iterator, AsyncIterator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def consume_sync_iterator(iterator: Iterator[Any]) -> None:
|
|
6
|
+
for _ in iterator:
|
|
7
|
+
...
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None:
|
|
11
|
+
async for _ in iterator:
|
|
12
|
+
...
|
linq/_utils/_sync.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
from typing import TypeVar, Callable, Awaitable
|
|
6
|
+
from typing_extensions import ParamSpec
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
import sniffio
|
|
10
|
+
import anyio.to_thread
|
|
11
|
+
|
|
12
|
+
T_Retval = TypeVar("T_Retval")
|
|
13
|
+
T_ParamSpec = ParamSpec("T_ParamSpec")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def to_thread(
|
|
17
|
+
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
|
|
18
|
+
) -> T_Retval:
|
|
19
|
+
if sniffio.current_async_library() == "asyncio":
|
|
20
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
21
|
+
|
|
22
|
+
return await anyio.to_thread.run_sync(
|
|
23
|
+
functools.partial(func, *args, **kwargs),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# inspired by `asyncer`, https://github.com/tiangolo/asyncer
|
|
28
|
+
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
|
|
29
|
+
"""
|
|
30
|
+
Take a blocking function and create an async one that receives the same
|
|
31
|
+
positional and keyword arguments.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
def blocking_func(arg1, arg2, kwarg1=None):
|
|
37
|
+
# blocking code
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Arguments
|
|
45
|
+
|
|
46
|
+
`function`: a blocking regular callable (e.g. a function)
|
|
47
|
+
|
|
48
|
+
## Return
|
|
49
|
+
|
|
50
|
+
An async function that takes the same positional and keyword arguments as the
|
|
51
|
+
original one, that when called runs the same original function in a thread worker
|
|
52
|
+
and returns the result.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
|
|
56
|
+
return await to_thread(function, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return wrapper
|