fancy-microsoft-teams 0.2.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.
- fancy_microsoft_teams/__init__.py +41 -0
- fancy_microsoft_teams/_fake.py +170 -0
- fancy_microsoft_teams/_runtime.py +569 -0
- fancy_microsoft_teams/actions/__init__.py +18 -0
- fancy_microsoft_teams/actions/meeting_create.py +156 -0
- fancy_microsoft_teams/actions/meeting_get.py +86 -0
- fancy_microsoft_teams/actions/transcript_list.py +88 -0
- fancy_microsoft_teams/faker.py +163 -0
- fancy_microsoft_teams/py.typed +0 -0
- fancy_microsoft_teams/service.py +73 -0
- fancy_microsoft_teams-0.2.0.dist-info/METADATA +154 -0
- fancy_microsoft_teams-0.2.0.dist-info/RECORD +14 -0
- fancy_microsoft_teams-0.2.0.dist-info/WHEEL +4 -0
- fancy_microsoft_teams-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# GENERATED FILE — do not edit.
|
|
2
|
+
#
|
|
3
|
+
# Emitted from provider/manifest.json by weaver's generator.
|
|
4
|
+
# A hand-edit here is destroyed by the next protocol sync, which is worse than
|
|
5
|
+
# being rejected, because it works until it silently does not. Fix
|
|
6
|
+
# provider/manifest.json (or weaver's template/) and regenerate:
|
|
7
|
+
#
|
|
8
|
+
# npm run provider -- microsoft_teams
|
|
9
|
+
|
|
10
|
+
"""Microsoft Teams for Python.
|
|
11
|
+
|
|
12
|
+
The service descriptor, its faker, its delivery contract, and one function
|
|
13
|
+
per operation — plain HTTP on the stdlib, no vendor SDK and no runtime
|
|
14
|
+
dependency.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from ._fake import FakeValues
|
|
20
|
+
from .actions.meeting_create import meeting_create
|
|
21
|
+
from .actions.meeting_get import meeting_get
|
|
22
|
+
from .actions.transcript_list import transcript_list
|
|
23
|
+
from .faker import respond
|
|
24
|
+
from .service import BASE_URLS, CONNECTOR_API_VERSION, REQUIRES, SANDBOX, SERVICE, TITLE, descriptor
|
|
25
|
+
|
|
26
|
+
__version__ = "0.2.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"BASE_URLS",
|
|
30
|
+
"CONNECTOR_API_VERSION",
|
|
31
|
+
"REQUIRES",
|
|
32
|
+
"SANDBOX",
|
|
33
|
+
"SERVICE",
|
|
34
|
+
"TITLE",
|
|
35
|
+
"FakeValues",
|
|
36
|
+
"descriptor",
|
|
37
|
+
"meeting_create",
|
|
38
|
+
"meeting_get",
|
|
39
|
+
"respond",
|
|
40
|
+
"transcript_list",
|
|
41
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# GENERATED FILE — do not edit.
|
|
2
|
+
#
|
|
3
|
+
# Emitted from provider/manifest.json (via weaver's
|
|
4
|
+
# template/embed/py/_fake.py) by weaver's generator.
|
|
5
|
+
# A hand-edit here is destroyed by the next protocol sync, which is worse than
|
|
6
|
+
# being rejected, because it works until it silently does not. Fix
|
|
7
|
+
# provider/manifest.json (via weaver's template/embed/py/_fake.py) (or
|
|
8
|
+
# weaver's template/) and regenerate:
|
|
9
|
+
#
|
|
10
|
+
# npm run provider -- microsoft_teams
|
|
11
|
+
|
|
12
|
+
"""Deterministic faker values — bit-for-bit with TypeScript and PHP.
|
|
13
|
+
|
|
14
|
+
PROVENANCE — read before editing.
|
|
15
|
+
|
|
16
|
+
This is the SINGLE SOURCE for the Python faker helpers. It exists because
|
|
17
|
+
``fancy-connector-core`` has a TypeScript implementation and a PHP twin and
|
|
18
|
+
**no Python twin at all**, so a generated ``fancy-<provider>`` package has no
|
|
19
|
+
shared runtime to import. Every generated package carries a copy emitted from
|
|
20
|
+
this file, and ``new-provider.mjs --check`` fails CI when a copy differs.
|
|
21
|
+
|
|
22
|
+
The permanent fix is a Python ``fancy-connector-core``. When it exists, this
|
|
23
|
+
file becomes a re-export and every provider picks it up on the next protocol
|
|
24
|
+
sync.
|
|
25
|
+
|
|
26
|
+
## Bit-for-bit identical is the whole point
|
|
27
|
+
|
|
28
|
+
Not "similar": the same FNV-1a seed and the same xorshift32 sequence, so a
|
|
29
|
+
golden fixture asserts the exact faked payload and ALL THREE runtimes have to
|
|
30
|
+
produce it. That turns the faker into a parity test rather than a convenience —
|
|
31
|
+
which matters, because cross-runtime drift does not fail loudly. It completes,
|
|
32
|
+
down one path, with no error.
|
|
33
|
+
|
|
34
|
+
Python integers are unbounded, so every 32-bit operation is masked back into
|
|
35
|
+
range. Dropping one of those masks does not break anything visibly; it just
|
|
36
|
+
makes the runtimes diverge after a few hundred calls, which is the worst
|
|
37
|
+
possible way for this to fail.
|
|
38
|
+
|
|
39
|
+
## Deterministic, and obviously fake
|
|
40
|
+
|
|
41
|
+
Same inputs, same output — always. A faker returning a fresh uuid every call
|
|
42
|
+
cannot be asserted on, so its fixtures degrade to "it did not throw", which is
|
|
43
|
+
the assertion that catches nothing. And the values are obviously synthetic ON
|
|
44
|
+
PURPOSE — ``fake_``-prefixed ids, ``example.test`` hosts, round numbers. Nobody
|
|
45
|
+
should ever look at a faked result and wonder whether it moved real money.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
from datetime import UTC, datetime, timedelta
|
|
52
|
+
from typing import Any
|
|
53
|
+
|
|
54
|
+
MASK = 0xFFFFFFFF
|
|
55
|
+
|
|
56
|
+
# `FakeValues.int` is part of the cross-runtime faker API — `fake.int(min, max)`
|
|
57
|
+
# in TypeScript, PHP and here — so the method keeps that name. Inside the class
|
|
58
|
+
# body it then shadows the builtin, and every LATER annotation reading `int`
|
|
59
|
+
# resolves to the method instead of the type. This alias is what the annotations
|
|
60
|
+
# after it use.
|
|
61
|
+
_Int = int
|
|
62
|
+
|
|
63
|
+
#: The instant every faker counts from. A constant rather than the clock,
|
|
64
|
+
#: because a fixture asserting on ``created`` must not start failing tomorrow.
|
|
65
|
+
FAKE_EPOCH = "2026-01-01T00:00:00.000Z"
|
|
66
|
+
|
|
67
|
+
_FNV_OFFSET = 0x811C9DC5
|
|
68
|
+
_FNV_PRIME = 0x01000193
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _stable_json(value: Any) -> str:
|
|
72
|
+
"""Render a value the way ``JSON.stringify`` would, with sorted object keys.
|
|
73
|
+
|
|
74
|
+
Key order must not change a seed. ``json.dumps`` preserves insertion order,
|
|
75
|
+
so ``{a, b}`` and ``{b, a}`` would hash differently and "same inputs, same
|
|
76
|
+
output" would hold only for dicts that happened to be built in the same
|
|
77
|
+
order — the kind of almost-true that survives review and fails in a fixture
|
|
78
|
+
months later.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(value, dict):
|
|
81
|
+
parts = [
|
|
82
|
+
f"{json.dumps(str(key))}:{_stable_json(item)}"
|
|
83
|
+
for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))
|
|
84
|
+
if item is not ...
|
|
85
|
+
]
|
|
86
|
+
return "{" + ",".join(parts) + "}"
|
|
87
|
+
|
|
88
|
+
if isinstance(value, (list, tuple)):
|
|
89
|
+
return "[" + ",".join(_stable_json(item) for item in value) + "]"
|
|
90
|
+
|
|
91
|
+
# `separators` matters: JavaScript emits no spaces, and a space here would
|
|
92
|
+
# change every seed.
|
|
93
|
+
return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def seed_from(*parts: Any) -> int:
|
|
97
|
+
"""FNV-1a over the stable rendering of the parts. Twin of ``seedFrom``."""
|
|
98
|
+
text = "|".join(part if isinstance(part, str) else _stable_json(part) for part in parts)
|
|
99
|
+
|
|
100
|
+
hash_ = _FNV_OFFSET
|
|
101
|
+
# JavaScript hashes UTF-16 code units, so a character outside the BMP
|
|
102
|
+
# contributes two of them. Encoding to UTF-16-LE and reading pairs is what
|
|
103
|
+
# keeps a provider name with an emoji in it seeding identically.
|
|
104
|
+
for unit in _utf16_units(text):
|
|
105
|
+
hash_ ^= unit
|
|
106
|
+
hash_ = (hash_ * _FNV_PRIME) & MASK
|
|
107
|
+
|
|
108
|
+
return hash_
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _utf16_units(text: str) -> list[int]:
|
|
112
|
+
raw = text.encode("utf-16-le")
|
|
113
|
+
|
|
114
|
+
return [raw[i] | (raw[i + 1] << 8) for i in range(0, len(raw), 2)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def seed_for_call(service: str, operation: str, config: dict[str, Any] | None) -> int:
|
|
118
|
+
"""The seed for one faked call: service, operation, and the caller's config."""
|
|
119
|
+
return seed_from(service, operation, config or {})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class FakeValues:
|
|
123
|
+
"""Deterministic value helpers handed to a faker.
|
|
124
|
+
|
|
125
|
+
Small on purpose. A faker's job is to return the SHAPE the provider returns
|
|
126
|
+
— the field names a downstream node will reference — not to simulate the
|
|
127
|
+
provider's business logic.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, seed: int) -> None:
|
|
131
|
+
masked = seed & MASK
|
|
132
|
+
self._state = masked if masked != 0 else 0x9E3779B9
|
|
133
|
+
|
|
134
|
+
def _next(self) -> int:
|
|
135
|
+
"""xorshift32, matching the JS generator step for step."""
|
|
136
|
+
state = self._state
|
|
137
|
+
state ^= (state << 13) & MASK
|
|
138
|
+
state &= MASK
|
|
139
|
+
state ^= state >> 17
|
|
140
|
+
state ^= (state << 5) & MASK
|
|
141
|
+
state &= MASK
|
|
142
|
+
self._state = state
|
|
143
|
+
|
|
144
|
+
return state
|
|
145
|
+
|
|
146
|
+
def hex(self, length: _Int) -> str:
|
|
147
|
+
"""A stable lowercase hex string of ``length`` characters."""
|
|
148
|
+
out = ""
|
|
149
|
+
while len(out) < length:
|
|
150
|
+
out += format(self._next(), "08x")
|
|
151
|
+
|
|
152
|
+
return out[:length]
|
|
153
|
+
|
|
154
|
+
def id(self, prefix: str) -> str:
|
|
155
|
+
"""A stable id with the provider's usual prefix: ``id("ch")`` -> ``ch_fake_1a2b3c``."""
|
|
156
|
+
return f"{prefix}_fake_{self.hex(12)}"
|
|
157
|
+
|
|
158
|
+
def int(self, minimum: int, maximum: int) -> int:
|
|
159
|
+
"""A stable integer in ``[minimum, maximum]``."""
|
|
160
|
+
return minimum + (self._next() % max(1, maximum - minimum + 1))
|
|
161
|
+
|
|
162
|
+
def pick(self, options: list[Any]) -> Any:
|
|
163
|
+
"""Pick a stable element of a list."""
|
|
164
|
+
return options[self._next() % len(options)]
|
|
165
|
+
|
|
166
|
+
def timestamp(self, offset_seconds: _Int = 0) -> str:
|
|
167
|
+
"""A fixed ISO-8601 instant, offset by whole seconds. Never ``now()``."""
|
|
168
|
+
base = datetime(2026, 1, 1, tzinfo=UTC) + timedelta(seconds=offset_seconds)
|
|
169
|
+
|
|
170
|
+
return base.strftime("%Y-%m-%dT%H:%M:%S") + ".000Z"
|