atsfill 0.0.3.dev5__tar.gz → 0.0.3.dev6__tar.gz
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.
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/PKG-INFO +5 -1
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/README.md +4 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/pyproject.toml +1 -1
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/pyproject.toml.orig +1 -1
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/commands.py +35 -16
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/registry.py +73 -11
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/results.py +2 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/registry.toml +7 -5
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/LICENSE +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.3.dev5 → atsfill-0.0.3.dev6}/src/atsfill/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atsfill
|
|
3
|
-
Version: 0.0.3.
|
|
3
|
+
Version: 0.0.3.dev6
|
|
4
4
|
Summary: Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle.
|
|
5
5
|
Keywords: job-applications,ats,ashby,greenhouse,teamtailor,ai-agents,cli
|
|
6
6
|
Author: shbernal
|
|
@@ -52,6 +52,10 @@ It is built for agents with a shell: every command prints one JSON document and
|
|
|
52
52
|
|
|
53
53
|
## Supported hiring systems
|
|
54
54
|
|
|
55
|
+
<!-- badges -->
|
|
56
|
+
   
|
|
57
|
+
<!-- /badges -->
|
|
58
|
+
|
|
55
59
|
| System | Account needed | `read` without a browser |
|
|
56
60
|
| --- | --- | --- |
|
|
57
61
|
| Ashby | no | yes |
|
|
@@ -28,6 +28,10 @@ It is built for agents with a shell: every command prints one JSON document and
|
|
|
28
28
|
|
|
29
29
|
## Supported hiring systems
|
|
30
30
|
|
|
31
|
+
<!-- badges -->
|
|
32
|
+
   
|
|
33
|
+
<!-- /badges -->
|
|
34
|
+
|
|
31
35
|
| System | Account needed | `read` without a browser |
|
|
32
36
|
| --- | --- | --- |
|
|
33
37
|
| Ashby | no | yes |
|
|
@@ -30,12 +30,14 @@ def _login_required(url: str, adapter: Adapter) -> Result:
|
|
|
30
30
|
|
|
31
31
|
@contextmanager
|
|
32
32
|
def _opened(
|
|
33
|
-
data: Path, url: str, sessions: Sessions | None
|
|
33
|
+
data: Path, url: str, sessions: Sessions | None, detected: list[str]
|
|
34
34
|
) -> Generator[tuple[Page, Adapter] | Result]:
|
|
35
35
|
"""The loaded page and the adapter that claims it by its URL, else by its page; or the
|
|
36
36
|
result that ends the command first. An adapter that needs an account gets its session,
|
|
37
|
-
and only that adapter's (ADR 0007)."""
|
|
37
|
+
and only that adapter's (ADR 0007). The adapter's name goes in `detected` once known."""
|
|
38
38
|
adapter = _detect(url)
|
|
39
|
+
if adapter:
|
|
40
|
+
detected.append(adapter.name)
|
|
39
41
|
session = None
|
|
40
42
|
if isinstance(adapter, NeedsAccount):
|
|
41
43
|
session = (sessions or {}).get(adapter.name) or accounts.session_file(data, adapter.name)
|
|
@@ -44,7 +46,8 @@ def _opened(
|
|
|
44
46
|
return
|
|
45
47
|
with open_page(data, session) as page:
|
|
46
48
|
page.goto(url)
|
|
47
|
-
adapter
|
|
49
|
+
if adapter is None and (adapter := _detect(url, page)):
|
|
50
|
+
detected.append(adapter.name)
|
|
48
51
|
if adapter is None:
|
|
49
52
|
yield _no_adapter(url)
|
|
50
53
|
elif isinstance(adapter, NeedsAccount) and not (
|
|
@@ -77,16 +80,18 @@ def _stops(result: Result) -> Generator[None]:
|
|
|
77
80
|
result.status, result.reason = Status.BLOCKED, f"covered: {e}"
|
|
78
81
|
|
|
79
82
|
|
|
80
|
-
def _in_budget(url: str, command: Callable[[], Result]) -> Result:
|
|
83
|
+
def _in_budget(url: str, command: Callable[[list[str]], Result]) -> Result:
|
|
81
84
|
"""Runs a command to a status whatever happens: past the click it reports `unknown` itself,
|
|
82
|
-
before it an error the adapter did not expect means the page is not one it handles.
|
|
85
|
+
before it an error the adapter did not expect means the page is not one it handles. The
|
|
86
|
+
result keeps the adapter detected before the stop."""
|
|
87
|
+
detected: list[str] = []
|
|
83
88
|
try:
|
|
84
|
-
return command()
|
|
89
|
+
return command(detected)
|
|
85
90
|
except OutOfTime as e:
|
|
86
|
-
return Result(Status.BLOCKED, url,
|
|
91
|
+
return Result(Status.BLOCKED, url, (detected or [None])[-1], f"time_budget: {e}")
|
|
87
92
|
except Exception as e:
|
|
88
93
|
reason = f"adapter_error: {type(e).__name__}: {e}"
|
|
89
|
-
return Result(Status.UNSUPPORTED, url, reason
|
|
94
|
+
return Result(Status.UNSUPPORTED, url, (detected or [None])[-1], reason)
|
|
90
95
|
|
|
91
96
|
|
|
92
97
|
def _fill(page: Page, adapter: Adapter, url: str, answers: dict[str, Answer]) -> Result:
|
|
@@ -104,8 +109,8 @@ def _fill(page: Page, adapter: Adapter, url: str, answers: dict[str, Answer]) ->
|
|
|
104
109
|
return result
|
|
105
110
|
|
|
106
111
|
|
|
107
|
-
def _read_page(data: Path, url: str, sessions: Sessions | None) -> Result:
|
|
108
|
-
with _opened(data, url, sessions) as opened:
|
|
112
|
+
def _read_page(data: Path, url: str, sessions: Sessions | None, detected: list[str]) -> Result:
|
|
113
|
+
with _opened(data, url, sessions, detected) as opened:
|
|
109
114
|
if isinstance(opened, Result):
|
|
110
115
|
return opened
|
|
111
116
|
page, adapter = opened
|
|
@@ -122,8 +127,9 @@ def _form_command(
|
|
|
122
127
|
sessions: Sessions | None,
|
|
123
128
|
send: bool,
|
|
124
129
|
resend: bool,
|
|
130
|
+
detected: list[str],
|
|
125
131
|
) -> Result:
|
|
126
|
-
with _opened(data, url, sessions) as opened:
|
|
132
|
+
with _opened(data, url, sessions, detected) as opened:
|
|
127
133
|
if isinstance(opened, Result):
|
|
128
134
|
return opened
|
|
129
135
|
page, adapter = opened
|
|
@@ -165,7 +171,7 @@ def read(url: str, *, data_dir: Path | None = None, sessions: Sessions | None =
|
|
|
165
171
|
result.fields = fields
|
|
166
172
|
return result
|
|
167
173
|
data = paths.data_dir(data_dir)
|
|
168
|
-
return _in_budget(url, lambda: _read_page(data, url, sessions))
|
|
174
|
+
return _in_budget(url, lambda seen: _read_page(data, url, sessions, seen))
|
|
169
175
|
|
|
170
176
|
|
|
171
177
|
def fill(
|
|
@@ -177,7 +183,7 @@ def fill(
|
|
|
177
183
|
) -> Result:
|
|
178
184
|
data = paths.data_dir(data_dir)
|
|
179
185
|
return _in_budget(
|
|
180
|
-
url, lambda: _form_command(data, url, answers, sessions,
|
|
186
|
+
url, lambda seen: _form_command(data, url, answers, sessions, False, False, seen)
|
|
181
187
|
)
|
|
182
188
|
|
|
183
189
|
|
|
@@ -191,13 +197,16 @@ def send(
|
|
|
191
197
|
) -> Result:
|
|
192
198
|
data = paths.data_dir(data_dir)
|
|
193
199
|
return _in_budget(
|
|
194
|
-
url, lambda: _form_command(data, url, answers, sessions,
|
|
200
|
+
url, lambda seen: _form_command(data, url, answers, sessions, True, resend, seen)
|
|
195
201
|
)
|
|
196
202
|
|
|
197
203
|
|
|
198
204
|
def _unverified(adapter: str) -> str | None:
|
|
199
|
-
"""The warning a send carries when no send through the adapter was verified
|
|
205
|
+
"""The warning a send carries when no send through the adapter was verified, or the last
|
|
206
|
+
verified one fails now (ADR 0012)."""
|
|
200
207
|
system = registry.for_adapter(adapter)
|
|
208
|
+
if system and system.send_broken:
|
|
209
|
+
return f"send_broken: {_broken(system)}"
|
|
201
210
|
if system and system.send_verified:
|
|
202
211
|
return None
|
|
203
212
|
state = system.state if system else None
|
|
@@ -223,16 +232,26 @@ def status(*, data_dir: Path | None = None) -> StatusReport:
|
|
|
223
232
|
)
|
|
224
233
|
|
|
225
234
|
|
|
235
|
+
def _broken(system: registry.System) -> str:
|
|
236
|
+
broken, last = system.send_broken, system.send_verified
|
|
237
|
+
assert broken
|
|
238
|
+
since = f"send broken since {broken.date} ({broken.version}), {broken.reason}"
|
|
239
|
+
return since + (f"; last verified {last.date} ({last.version})" if last else "")
|
|
240
|
+
|
|
241
|
+
|
|
226
242
|
def _adapter_state(adapter: Adapter, data: Path) -> AdapterState:
|
|
227
243
|
system = registry.for_adapter(adapter.name)
|
|
228
244
|
verified = system.state if system else None
|
|
245
|
+
v = getattr(system, verified) if system and verified else None
|
|
229
246
|
return AdapterState(
|
|
230
247
|
adapter.name,
|
|
231
248
|
submit_enabled=paths.submit_enabled(adapter.name),
|
|
232
249
|
side_effects=adapter.side_effects,
|
|
233
250
|
side_effects_acknowledged=paths.acknowledged(adapter.name),
|
|
234
251
|
verified=verified,
|
|
235
|
-
verified_version=
|
|
252
|
+
verified_version=v.version if v else None,
|
|
253
|
+
verified_release=registry.release(v.version, version("atsfill")) if v else None,
|
|
254
|
+
send_broken=_broken(system) if system and system.send_broken else None,
|
|
236
255
|
session=_session_state(adapter, data),
|
|
237
256
|
)
|
|
238
257
|
|
|
@@ -12,13 +12,28 @@ HOSTING = ("hosted", "embedded")
|
|
|
12
12
|
VENDORS = ("DataDome", "Cloudflare", "Akamai", "PerimeterX", "unknown")
|
|
13
13
|
STEPS = ("posting", "form", "upload")
|
|
14
14
|
STATES = ("mapped", "dry_run_verified", "send_verified")
|
|
15
|
+
CAUSES = ("system", "code", "unknown")
|
|
16
|
+
RELEASES = ("this_release", "older", "newer")
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
@dataclass(frozen=True)
|
|
18
20
|
class Verification:
|
|
21
|
+
date: date
|
|
22
|
+
version: str # the release, compared with the installed one
|
|
23
|
+
by: str
|
|
24
|
+
commit: str | None = None # the exact code
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Broken:
|
|
29
|
+
"""A send that worked fails now (ADR 0012); `reason` names fields and steps, never postings."""
|
|
30
|
+
|
|
19
31
|
date: date
|
|
20
32
|
version: str
|
|
33
|
+
commit: str
|
|
21
34
|
by: str
|
|
35
|
+
reason: str
|
|
36
|
+
cause: str = "unknown"
|
|
22
37
|
|
|
23
38
|
|
|
24
39
|
@dataclass(frozen=True)
|
|
@@ -43,7 +58,8 @@ class System:
|
|
|
43
58
|
block: Block | None = None
|
|
44
59
|
mapped: Verification | None = None
|
|
45
60
|
dry_run_verified: Verification | None = None
|
|
46
|
-
send_verified: Verification | None = None
|
|
61
|
+
send_verified: Verification | None = None # the last known good, kept through a break
|
|
62
|
+
send_broken: Broken | None = None
|
|
47
63
|
|
|
48
64
|
@property
|
|
49
65
|
def state(self) -> str | None:
|
|
@@ -70,6 +86,10 @@ def _system(source: str, entry: dict) -> System:
|
|
|
70
86
|
_one_of(source, "block.step", block.get("step"), STEPS)
|
|
71
87
|
block = Block(**block)
|
|
72
88
|
states = {s: Verification(**entry[s]) for s in STATES if s in entry}
|
|
89
|
+
broken = entry.get("send_broken")
|
|
90
|
+
if broken is not None:
|
|
91
|
+
_one_of(source, "send_broken.cause", broken.get("cause", "unknown"), CAUSES)
|
|
92
|
+
broken = Broken(**broken)
|
|
73
93
|
return System(
|
|
74
94
|
source,
|
|
75
95
|
entry["kind"],
|
|
@@ -79,6 +99,7 @@ def _system(source: str, entry: dict) -> System:
|
|
|
79
99
|
entry.get("share"),
|
|
80
100
|
block,
|
|
81
101
|
**states,
|
|
102
|
+
send_broken=broken,
|
|
82
103
|
)
|
|
83
104
|
|
|
84
105
|
|
|
@@ -91,20 +112,36 @@ def systems() -> dict[str, System]:
|
|
|
91
112
|
return parse(files("atsfill").joinpath("registry.toml").read_text())
|
|
92
113
|
|
|
93
114
|
|
|
94
|
-
def
|
|
95
|
-
|
|
115
|
+
def _toml(value: object) -> str:
|
|
116
|
+
return str(value) if isinstance(value, date) else f'"{value}"'
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def with_state(text: str, source: str, key: str, value: Verification | Broken) -> str:
|
|
120
|
+
"""The registry file's text with `key` of `source` set to `value`, the rest untouched.
|
|
121
|
+
Writing `send_verified` ends a `send_broken`."""
|
|
96
122
|
lines = text.splitlines()
|
|
97
123
|
start = lines.index(f"[{source}]")
|
|
98
124
|
end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines))
|
|
99
125
|
while lines[end - 1] == "":
|
|
100
126
|
end -= 1
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
gone = {key, "send_broken"} if key == "send_verified" else {key}
|
|
128
|
+
entry = lines[start:end]
|
|
129
|
+
at = next((i for i, x in enumerate(entry) if x.split(" =")[0] in gone), len(entry))
|
|
130
|
+
entry = [x for x in entry if x.split(" =")[0] not in gone]
|
|
131
|
+
pairs = ", ".join(
|
|
132
|
+
f"{f.name} = {_toml(getattr(value, f.name))}"
|
|
133
|
+
for f in fields(value)
|
|
134
|
+
if getattr(value, f.name) is not None
|
|
135
|
+
)
|
|
136
|
+
entry.insert(min(at, len(entry)), f"{key} = {{ {pairs} }}")
|
|
137
|
+
return "\n".join(lines[:start] + entry + lines[end:]) + "\n"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def release(verified: str, installed: str) -> str:
|
|
141
|
+
"""Whether a verified release is the installed one, `older` or `newer`."""
|
|
142
|
+
as_tuple = lambda v: tuple(int(n) for n in v.split("+")[0].split(".")) # noqa: E731
|
|
143
|
+
a, b = as_tuple(verified), as_tuple(installed)
|
|
144
|
+
return "this_release" if a == b else "older" if a < b else "newer"
|
|
108
145
|
|
|
109
146
|
|
|
110
147
|
def for_adapter(name: str) -> System | None:
|
|
@@ -118,7 +155,7 @@ def _backlog_key(s: System) -> tuple:
|
|
|
118
155
|
def _row(s: System) -> str:
|
|
119
156
|
share = f"{s.share:.1%}" if s.share is not None else ""
|
|
120
157
|
block = f"{s.block.vendor} at {s.block.step}" if s.block else ""
|
|
121
|
-
state = s.state or ""
|
|
158
|
+
state = "send_broken" if s.send_broken else s.state or ""
|
|
122
159
|
if s.adapter:
|
|
123
160
|
state = f"[{state}]({s.adapter}.md)" if state else f"[adapter]({s.adapter}.md)"
|
|
124
161
|
cells = [s.source, s.kind, share, s.account, ", ".join(s.hosting), block, state]
|
|
@@ -151,3 +188,28 @@ def backlog(registry: dict[str, System] | None = None) -> str:
|
|
|
151
188
|
text += ["", "Aggregators and company-built forms, counted and not adapted:", "", head]
|
|
152
189
|
text += map(_row, others)
|
|
153
190
|
return "\n".join(text) + "\n"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
BADGES = ("<!-- badges -->", "<!-- /badges -->")
|
|
194
|
+
COLORS = {"send_verified": "9ece6a", "dry_run_verified": "7aa2f7", "mapped": "a9b1d6"}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _badge(s: System) -> str:
|
|
198
|
+
if s.send_broken:
|
|
199
|
+
message, color = f"send broken {s.send_broken.date}", "f7768e"
|
|
200
|
+
elif s.state:
|
|
201
|
+
v: Verification = getattr(s, s.state)
|
|
202
|
+
message, color = f"{s.state.replace('_', ' ')} {v.date}", COLORS[s.state]
|
|
203
|
+
else:
|
|
204
|
+
message, color = "unverified", "565f89"
|
|
205
|
+
escape = lambda t: t.replace("-", "--").replace("_", "__").replace(" ", "%20") # noqa: E731
|
|
206
|
+
url = f"https://img.shields.io/badge/{escape(s.adapter or s.source)}-{escape(message)}-{color}"
|
|
207
|
+
return f""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def with_badges(readme: str, registry: dict[str, System] | None = None) -> str:
|
|
211
|
+
"""The README with one badge per adapter between its badge markers."""
|
|
212
|
+
registry = systems() if registry is None else registry
|
|
213
|
+
start, end = readme.index(BADGES[0]) + len(BADGES[0]), readme.index(BADGES[1])
|
|
214
|
+
badges = " ".join(_badge(s) for s in sorted(registry.values(), key=_backlog_key) if s.adapter)
|
|
215
|
+
return f"{readme[:start]}\n{badges}\n{readme[end:]}"
|
|
@@ -72,6 +72,8 @@ class AdapterState:
|
|
|
72
72
|
side_effects_acknowledged: bool
|
|
73
73
|
verified: str | None = None # mapped | dry_run_verified | send_verified (ADR 0012)
|
|
74
74
|
verified_version: str | None = None
|
|
75
|
+
verified_release: str | None = None # this_release | older | newer than the installed one
|
|
76
|
+
send_broken: str | None = None # since when and why, with the last verified send
|
|
75
77
|
session: str | None = None # signed_in | signed_out | missing, for a system with accounts
|
|
76
78
|
|
|
77
79
|
|
|
@@ -8,32 +8,34 @@
|
|
|
8
8
|
# adapter the adapter's name, when there is one
|
|
9
9
|
# block vendor, signal, step (posting | form | upload), date, version, camoufox, browser
|
|
10
10
|
# mapped, dry_run_verified, send_verified
|
|
11
|
-
# date, version, by; only the user sets
|
|
11
|
+
# date, version (the release), commit (the exact code), by; only the user sets
|
|
12
|
+
# send_verified
|
|
13
|
+
# send_broken
|
|
14
|
+
# date, version, commit, by, cause (system | code | unknown), reason: a send that
|
|
15
|
+
# worked fails now; send_verified stays as the last known good
|
|
12
16
|
|
|
13
17
|
[ashby]
|
|
14
18
|
kind = "ats"
|
|
15
19
|
account = "none"
|
|
16
20
|
hosting = ["hosted", "embedded"]
|
|
17
21
|
adapter = "ashby"
|
|
18
|
-
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
22
|
+
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal", commit = "ff8ea46" }
|
|
23
|
+
send_broken = { date = 2026-10-19, version = "0.0.2", commit = "916319c", by = "shbernal", reason = "the résumé reads back empty after its upload", cause = "unknown" }
|
|
19
24
|
|
|
20
25
|
[teamtailor]
|
|
21
26
|
kind = "ats"
|
|
22
27
|
account = "none"
|
|
23
28
|
hosting = ["hosted"]
|
|
24
29
|
adapter = "teamtailor"
|
|
25
|
-
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
26
30
|
|
|
27
31
|
[greenhouse]
|
|
28
32
|
kind = "ats"
|
|
29
33
|
account = "none"
|
|
30
34
|
hosting = ["hosted", "embedded"]
|
|
31
35
|
adapter = "greenhouse"
|
|
32
|
-
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
33
36
|
|
|
34
37
|
[wttj]
|
|
35
38
|
kind = "board"
|
|
36
39
|
account = "to_send"
|
|
37
40
|
hosting = ["hosted"]
|
|
38
41
|
adapter = "wttj"
|
|
39
|
-
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|