funora 0.0.1.dev2__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.
- funora/__init__.py +265 -0
- funora/_account.py +661 -0
- funora/_aclient.py +1484 -0
- funora/_budget.py +579 -0
- funora/_calc.py +182 -0
- funora/_canonical.py +235 -0
- funora/_catalog.py +473 -0
- funora/_chat_history.py +367 -0
- funora/_chats.py +392 -0
- funora/_chips.py +403 -0
- funora/_classify.py +466 -0
- funora/_client.py +1490 -0
- funora/_currency_switch.py +130 -0
- funora/_cursor.py +120 -0
- funora/_delivered.py +233 -0
- funora/_diff.py +752 -0
- funora/_engine.py +4795 -0
- funora/_extract.py +124 -0
- funora/_field_schema.py +112 -0
- funora/_fileio.py +58 -0
- funora/_gate.py +69 -0
- funora/_hops.py +101 -0
- funora/_host.py +120 -0
- funora/_identity.py +284 -0
- funora/_json.py +31 -0
- funora/_listen.py +312 -0
- funora/_lot_form.py +331 -0
- funora/_market.py +406 -0
- funora/_matching.py +147 -0
- funora/_money.py +279 -0
- funora/_monitoring.py +396 -0
- funora/_observed.py +237 -0
- funora/_order.py +552 -0
- funora/_order_details.py +281 -0
- funora/_orders.py +807 -0
- funora/_outbound.py +453 -0
- funora/_own_lots.py +301 -0
- funora/_poll.py +410 -0
- funora/_price_audit.py +281 -0
- funora/_proxies.py +232 -0
- funora/_raise.py +150 -0
- funora/_refund.py +102 -0
- funora/_result.py +157 -0
- funora/_retry.py +213 -0
- funora/_review_write.py +138 -0
- funora/_reviews.py +584 -0
- funora/_runner.py +651 -0
- funora/_secret.py +385 -0
- funora/_showcase.py +362 -0
- funora/_signals.py +375 -0
- funora/_skeleton.py +752 -0
- funora/_snapshot.py +276 -0
- funora/_state.py +279 -0
- funora/_stock.py +32 -0
- funora/_thread.py +574 -0
- funora/_transport.py +1023 -0
- funora/_updates.py +292 -0
- funora/_verdicts.py +91 -0
- funora/_viewing.py +143 -0
- funora/_watch.py +825 -0
- funora/_watch_state.py +237 -0
- funora/_whoami.py +546 -0
- funora/bot/__init__.py +52 -0
- funora/bot/_delivery.py +341 -0
- funora/bot/_outbox.py +261 -0
- funora/bot/_runtime.py +447 -0
- funora/bot/_spool.py +534 -0
- funora/budget.py +311 -0
- funora/capabilities.py +297 -0
- funora/conformance.py +758 -0
- funora/contract.py +73 -0
- funora/errors.py +996 -0
- funora/events.py +189 -0
- funora/extraction.py +420 -0
- funora/observe.py +560 -0
- funora/operations.py +671 -0
- funora/py.typed +0 -0
- funora/reconciliation.py +51 -0
- funora/response_classes.py +155 -0
- funora/retry.py +238 -0
- funora/send_outcome.py +78 -0
- funora/skeleton_format.py +77 -0
- funora-0.0.1.dev2.dist-info/METADATA +294 -0
- funora-0.0.1.dev2.dist-info/RECORD +87 -0
- funora-0.0.1.dev2.dist-info/WHEEL +4 -0
- funora-0.0.1.dev2.dist-info/entry_points.txt +2 -0
- funora-0.0.1.dev2.dist-info/licenses/LICENSE +201 -0
funora/_observed.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Значение вместе с обстоятельствами его наблюдения.
|
|
2
|
+
|
|
3
|
+
Тип существует ради одного различия, и это различие стоит денег продавца.
|
|
4
|
+
Значение ``None`` одинаково выглядит для «поле пустое» и «поля на странице не
|
|
5
|
+
было», а решения по этим случаям противоположные. Пустое поле - наблюдение:
|
|
6
|
+
описание заказа действительно пустое, перечитывать нечего. Отсутствующее поле -
|
|
7
|
+
подозрение на изменённую разметку, и перечитать стоит.
|
|
8
|
+
|
|
9
|
+
Склеив их в ``None``, мы получили бы худший вид отказа: правдоподобный ответ, о
|
|
10
|
+
неверности которого узнать неоткуда.
|
|
11
|
+
|
|
12
|
+
Отсюда три правила, каждое из которых неудобно нарочно.
|
|
13
|
+
|
|
14
|
+
Чтение ``.value`` у ненаблюдённого значения бросает исключение, а не возвращает
|
|
15
|
+
``None``. Подставить умолчание может только вызывающий: лишь он знает, чем
|
|
16
|
+
грозит его задаче отсутствие именно этого поля.
|
|
17
|
+
|
|
18
|
+
Приведение к булеву значению запрещено. Три состояния не сводятся к двум без
|
|
19
|
+
потери ровно того различия, ради которого тип введён, а запись ``if entry.status``
|
|
20
|
+
выглядит настолько естественно, что молча проглотила бы его.
|
|
21
|
+
|
|
22
|
+
Строковое представление у ненаблюдённого значения показывает причину, а не
|
|
23
|
+
пустоту. Значение уходит в журналы и в сообщения, и «» там неотличимо от честно
|
|
24
|
+
пустой строки.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from enum import StrEnum
|
|
31
|
+
from typing import Generic, NoReturn, TypeVar
|
|
32
|
+
|
|
33
|
+
from .errors import UnobservedFieldError
|
|
34
|
+
|
|
35
|
+
__all__ = ["Confidence", "Presence", "Observed"]
|
|
36
|
+
|
|
37
|
+
#: Тип наблюдаемого значения.
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
#: Тип значения, подставляемого вызывающим.
|
|
41
|
+
D = TypeVar("D")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Confidence(StrEnum):
|
|
45
|
+
"""Уверенность правила извлечения.
|
|
46
|
+
|
|
47
|
+
Словарь взят из spec/extraction/rules.yaml. Уровня ``assumed`` здесь нет
|
|
48
|
+
намеренно: спецификация запрещает предположения в контракте, потому что
|
|
49
|
+
предположение в правиле извлечения даёт молчаливый отказ.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
OBSERVED = "observed"
|
|
53
|
+
INFERRED = "inferred"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Presence(StrEnum):
|
|
57
|
+
"""Что случилось с полем при разборе.
|
|
58
|
+
|
|
59
|
+
Значения различают три исхода, а не два. Разница между EMPTY и NOT_OBSERVED
|
|
60
|
+
определяет, имеет ли смысл перечитывать страницу.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
#: Место значения найдено, значение непустое.
|
|
64
|
+
PRESENT = "present"
|
|
65
|
+
|
|
66
|
+
#: Место значения найдено, значение пустое. Это наблюдение, а не пробел.
|
|
67
|
+
EMPTY = "empty"
|
|
68
|
+
|
|
69
|
+
#: Места значения не нашлось. Читать нечего.
|
|
70
|
+
NOT_OBSERVED = "not_observed"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class Observed(Generic[T]):
|
|
75
|
+
"""Значение поля вместе с обстоятельствами его наблюдения.
|
|
76
|
+
|
|
77
|
+
Собирается тремя именованными конструкторами, а не напрямую: прямой вызов
|
|
78
|
+
позволил бы собрать несогласованную запись, например наблюдённое значение
|
|
79
|
+
без уверенности.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
presence (Presence): Что случилось с полем при разборе.
|
|
83
|
+
confidence (Confidence | None): Уверенность правила извлечения. None,
|
|
84
|
+
если поле не наблюдалось: уверенность в отсутствии не определена.
|
|
85
|
+
reason (str | None): Машиночитаемая причина отсутствия. Заполнена только
|
|
86
|
+
при NOT_OBSERVED.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
presence: Presence
|
|
90
|
+
confidence: Confidence | None
|
|
91
|
+
reason: str | None
|
|
92
|
+
_value: T | None
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def present(cls, value: T, confidence: Confidence = Confidence.OBSERVED) -> Observed[T]:
|
|
96
|
+
"""Собирает наблюдённое непустое значение.
|
|
97
|
+
|
|
98
|
+
Непустоту обещает сам тип, и до сих пор обещание держалось на честном
|
|
99
|
+
слове: конструктор принимал что угодно. Собранное здесь пустое значение
|
|
100
|
+
отбирает у вызывающего единственный способ отличить «поле есть, и оно
|
|
101
|
+
пусто» от «поле есть», - а различать он должен по состоянию, а не
|
|
102
|
+
заглядывая внутрь.
|
|
103
|
+
|
|
104
|
+
Пустота проверяется только у строк и последовательностей. У логического
|
|
105
|
+
поля False - полноценное значение, а не пустота, и у числового ноль
|
|
106
|
+
тоже: там пустоты не бывает.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
value (T): Значение.
|
|
110
|
+
confidence (Confidence): Уверенность правила извлечения.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Observed[T]: Запись в состоянии PRESENT.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: Если строка или последовательность пуста. Это не
|
|
117
|
+
состояние площадки, а ошибка в разборе: для пустого значения
|
|
118
|
+
есть empty().
|
|
119
|
+
"""
|
|
120
|
+
if isinstance(value, str | tuple | list | frozenset | set | dict) and not value:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"Observed.present получил пустое значение; "
|
|
123
|
+
"для пустого наблюдения есть Observed.empty"
|
|
124
|
+
)
|
|
125
|
+
return cls(Presence.PRESENT, confidence, None, value)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def empty(cls, value: T, confidence: Confidence = Confidence.OBSERVED) -> Observed[T]:
|
|
129
|
+
"""Собирает наблюдённое пустое значение.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value (T): Пустое значение своего типа, например пустая строка.
|
|
133
|
+
confidence (Confidence): Уверенность правила извлечения.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Observed[T]: Запись в состоянии EMPTY.
|
|
137
|
+
"""
|
|
138
|
+
return cls(Presence.EMPTY, confidence, None, value)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def missing(cls, reason: str) -> Observed[T]:
|
|
142
|
+
"""Собирает запись о том, что места значения на странице не нашлось.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
reason (str): Машиночитаемая причина, например ``selector_no_match``
|
|
146
|
+
или ``status_mapping_not_observed``. Уходит в журнал и в текст
|
|
147
|
+
исключения, поэтому обязана быть понятна без контекста.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Observed[T]: Запись в состоянии NOT_OBSERVED.
|
|
151
|
+
"""
|
|
152
|
+
return cls(Presence.NOT_OBSERVED, None, reason, None)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_observed(self) -> bool:
|
|
156
|
+
"""Сообщает, нашлось ли место значения на странице.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
bool: True при PRESENT и EMPTY, False при NOT_OBSERVED.
|
|
160
|
+
"""
|
|
161
|
+
return self.presence is not Presence.NOT_OBSERVED
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def value(self) -> T:
|
|
165
|
+
"""Возвращает наблюдённое значение.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
T: Значение. При EMPTY возвращается пустое значение своего типа:
|
|
169
|
+
место найдено, и это тоже наблюдение.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
UnobservedFieldError: Если поле не наблюдалось. Вернуть здесь None
|
|
173
|
+
значило бы стереть разницу между «пусто» и «не было», ради
|
|
174
|
+
которой тип и заведён. Подставить умолчание может только
|
|
175
|
+
вызывающий, для этого есть get и or_none.
|
|
176
|
+
"""
|
|
177
|
+
if self.presence is Presence.NOT_OBSERVED:
|
|
178
|
+
raise UnobservedFieldError(
|
|
179
|
+
f"поле не наблюдалось на странице (причина: {self.reason}). "
|
|
180
|
+
"Подставьте значение через get(...) либо получите None через "
|
|
181
|
+
"or_none(), если отсутствие поля для вашей задачи допустимо"
|
|
182
|
+
)
|
|
183
|
+
return self._value # type: ignore[return-value]
|
|
184
|
+
|
|
185
|
+
def get(self, default: D) -> T | D:
|
|
186
|
+
"""Возвращает значение либо подставленное вызывающим.
|
|
187
|
+
|
|
188
|
+
Подстановка умолчания и есть явное признание того, что поле могло не
|
|
189
|
+
наблюдаться.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
default (D): Что вернуть, если поле не наблюдалось.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
T | D: Значение либо default.
|
|
196
|
+
"""
|
|
197
|
+
if self.presence is Presence.NOT_OBSERVED:
|
|
198
|
+
return default
|
|
199
|
+
return self._value # type: ignore[return-value]
|
|
200
|
+
|
|
201
|
+
def or_none(self) -> T | None:
|
|
202
|
+
"""Возвращает значение либо None, если поле не наблюдалось.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
T | None: Значение либо None.
|
|
206
|
+
"""
|
|
207
|
+
return self._value
|
|
208
|
+
|
|
209
|
+
def __bool__(self) -> NoReturn:
|
|
210
|
+
"""Запрещает приведение к булеву значению.
|
|
211
|
+
|
|
212
|
+
Три состояния не сводятся к двум без потери того различия, ради которого
|
|
213
|
+
тип введён. Запись ``if entry.description`` выглядит настолько
|
|
214
|
+
естественно, что проглотила бы его молча.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
TypeError: Всегда.
|
|
218
|
+
"""
|
|
219
|
+
raise TypeError(
|
|
220
|
+
"Observed нельзя привести к булеву значению: состояний три, а не два. "
|
|
221
|
+
"Спросите is_observed, если нужен факт наблюдения, либо сравните "
|
|
222
|
+
"presence с нужным значением"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def __str__(self) -> str:
|
|
226
|
+
"""Возвращает представление для человека.
|
|
227
|
+
|
|
228
|
+
Отсутствие показывается словами, а не пустотой: значение уходит в
|
|
229
|
+
журналы и сообщения, где пустая строка неотличима от честно пустого
|
|
230
|
+
поля.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
str: Значение, пустая строка либо пометка об отсутствии с причиной.
|
|
234
|
+
"""
|
|
235
|
+
if self.presence is Presence.NOT_OBSERVED:
|
|
236
|
+
return f"<не наблюдалось: {self.reason}>"
|
|
237
|
+
return str(self._value)
|