atsfill 0.0.3.dev3__tar.gz → 0.0.3.dev5__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.dev3 → atsfill-0.0.3.dev5}/PKG-INFO +4 -3
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/README.md +3 -2
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/pyproject.toml +1 -1
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/pyproject.toml.orig +1 -1
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/cli.py +2 -1
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/browser.py +4 -2
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/commands.py +27 -11
- atsfill-0.0.3.dev5/src/atsfill/core/registry.py +153 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/results.py +5 -1
- atsfill-0.0.3.dev5/src/atsfill/registry.toml +39 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/LICENSE +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.3.dev3 → atsfill-0.0.3.dev5}/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.dev5
|
|
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
|
|
@@ -124,7 +124,8 @@ Every push to `main` that passes CI is also published as a dev release; `uvx --p
|
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
A dry run is not invisible: some systems save answers or the resume as they are typed.
|
|
127
|
-
`atsfill status` lists each system's side effects
|
|
127
|
+
`atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
|
|
128
|
+
A `send` through an adapter no send was verified on goes ahead with a `warning`.
|
|
128
129
|
|
|
129
130
|
## How it works
|
|
130
131
|
|
|
@@ -147,7 +148,7 @@ The exit code is 0 for `ready` and `submitted`, 1 otherwise, 2 for a usage error
|
|
|
147
148
|
## More
|
|
148
149
|
|
|
149
150
|
- [Architecture decisions](docs/adr/README.md): the contract, the submit gate, the browser, sessions.
|
|
150
|
-
- [Hiring systems](docs/adapters/): each
|
|
151
|
+
- [Hiring systems](docs/adapters/README.md): every system mapped so far, in backlog order, and each one's form as observed.
|
|
151
152
|
- `atsfill --help` for every command and option.
|
|
152
153
|
|
|
153
154
|
[pypi]: https://pypi.org/project/atsfill/
|
|
@@ -100,7 +100,8 @@ Every push to `main` that passes CI is also published as a dev release; `uvx --p
|
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
A dry run is not invisible: some systems save answers or the resume as they are typed.
|
|
103
|
-
`atsfill status` lists each system's side effects
|
|
103
|
+
`atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
|
|
104
|
+
A `send` through an adapter no send was verified on goes ahead with a `warning`.
|
|
104
105
|
|
|
105
106
|
## How it works
|
|
106
107
|
|
|
@@ -123,7 +124,7 @@ The exit code is 0 for `ready` and `submitted`, 1 otherwise, 2 for a usage error
|
|
|
123
124
|
## More
|
|
124
125
|
|
|
125
126
|
- [Architecture decisions](docs/adr/README.md): the contract, the submit gate, the browser, sessions.
|
|
126
|
-
- [Hiring systems](docs/adapters/): each
|
|
127
|
+
- [Hiring systems](docs/adapters/README.md): every system mapped so far, in backlog order, and each one's form as observed.
|
|
127
128
|
- `atsfill --help` for every command and option.
|
|
128
129
|
|
|
129
130
|
[pypi]: https://pypi.org/project/atsfill/
|
|
@@ -103,7 +103,8 @@ def run(args: argparse.Namespace) -> int:
|
|
|
103
103
|
case "status":
|
|
104
104
|
report = core.status(data_dir=args.data_dir)
|
|
105
105
|
emit(report)
|
|
106
|
-
|
|
106
|
+
signed_out = any(a.session == "signed_out" for a in report.adapters)
|
|
107
|
+
return 1 if signed_out or report.browser != report.browser_pinned else 0
|
|
107
108
|
case "install-browser":
|
|
108
109
|
return browser.install()
|
|
109
110
|
case "login":
|
|
@@ -22,6 +22,7 @@ from playwright.sync_api import Error as BrowserError
|
|
|
22
22
|
from playwright.sync_api import TimeoutError as Timeout
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
|
+
"BUILD",
|
|
25
26
|
"BrowserError",
|
|
26
27
|
"Covered",
|
|
27
28
|
"Locator",
|
|
@@ -43,6 +44,7 @@ __all__ = [
|
|
|
43
44
|
MIN_SCREEN_HEIGHT = 1050 # leaves the window the 900px Greenhouse and Teamtailor need
|
|
44
45
|
HOST_OS = {"Linux": "linux", "Darwin": "macos", "Windows": "windows"}[platform.system()]
|
|
45
46
|
BUDGET = 180.0 # seconds per command, lock wait included
|
|
47
|
+
BUILD = "156.0.1-beta.36" # the Camoufox browser build, pinned so results compare across machines
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
class OutOfTime(Exception):
|
|
@@ -62,8 +64,8 @@ def version() -> str:
|
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def install() -> int:
|
|
65
|
-
"""Fetches the browser build
|
|
66
|
-
return subprocess.call([sys.executable, "-m", "camoufox", "fetch"])
|
|
67
|
+
"""Fetches the pinned browser build."""
|
|
68
|
+
return subprocess.call([sys.executable, "-m", "camoufox", "fetch", f"official/{BUILD}"])
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
def identity(data_dir: Path) -> dict:
|
|
@@ -4,7 +4,7 @@ from importlib.metadata import version
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from atsfill.adapters import ADAPTERS
|
|
7
|
-
from atsfill.core import accounts, attempts, browser, matching, paths
|
|
7
|
+
from atsfill.core import accounts, attempts, browser, matching, paths, registry
|
|
8
8
|
from atsfill.core.adapter import Adapter, Blocked, NeedsAccount, Unsupported
|
|
9
9
|
from atsfill.core.browser import Covered, OutOfTime, Page, open_page
|
|
10
10
|
from atsfill.core.results import (
|
|
@@ -138,6 +138,7 @@ def _form_command(
|
|
|
138
138
|
result = _fill(page, adapter, url, answers)
|
|
139
139
|
if not send or result.status is not Status.READY:
|
|
140
140
|
return result
|
|
141
|
+
result.warning = _unverified(adapter.name)
|
|
141
142
|
attempts.record(data, posting, attempts.PENDING, url=url)
|
|
142
143
|
try:
|
|
143
144
|
result.evidence = adapter.submit(page)
|
|
@@ -194,6 +195,15 @@ def send(
|
|
|
194
195
|
)
|
|
195
196
|
|
|
196
197
|
|
|
198
|
+
def _unverified(adapter: str) -> str | None:
|
|
199
|
+
"""The warning a send carries when no send through the adapter was verified (ADR 0012)."""
|
|
200
|
+
system = registry.for_adapter(adapter)
|
|
201
|
+
if system and system.send_verified:
|
|
202
|
+
return None
|
|
203
|
+
state = system.state if system else None
|
|
204
|
+
return f"not_send_verified: {adapter} is {state or 'not in the registry'}"
|
|
205
|
+
|
|
206
|
+
|
|
197
207
|
def _session_state(adapter: Adapter, data: Path) -> str | None:
|
|
198
208
|
if not isinstance(adapter, NeedsAccount):
|
|
199
209
|
return None
|
|
@@ -208,16 +218,22 @@ def status(*, data_dir: Path | None = None) -> StatusReport:
|
|
|
208
218
|
data_dir=data,
|
|
209
219
|
config=paths.config_file(),
|
|
210
220
|
browser=browser.version(),
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
browser_pinned=browser.BUILD,
|
|
222
|
+
adapters=[_adapter_state(a, data) for a in ADAPTERS],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _adapter_state(adapter: Adapter, data: Path) -> AdapterState:
|
|
227
|
+
system = registry.for_adapter(adapter.name)
|
|
228
|
+
verified = system.state if system else None
|
|
229
|
+
return AdapterState(
|
|
230
|
+
adapter.name,
|
|
231
|
+
submit_enabled=paths.submit_enabled(adapter.name),
|
|
232
|
+
side_effects=adapter.side_effects,
|
|
233
|
+
side_effects_acknowledged=paths.acknowledged(adapter.name),
|
|
234
|
+
verified=verified,
|
|
235
|
+
verified_version=getattr(system, verified).version if system and verified else None,
|
|
236
|
+
session=_session_state(adapter, data),
|
|
221
237
|
)
|
|
222
238
|
|
|
223
239
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""The committed registry of hiring systems (ADR 0012)."""
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from dataclasses import dataclass, fields
|
|
5
|
+
from datetime import date
|
|
6
|
+
from functools import cache
|
|
7
|
+
from importlib.resources import files
|
|
8
|
+
|
|
9
|
+
KINDS = ("ats", "board", "aggregator", "custom")
|
|
10
|
+
ACCOUNTS = ("none", "to_send", "to_read") # also the backlog order (ADR 0013)
|
|
11
|
+
HOSTING = ("hosted", "embedded")
|
|
12
|
+
VENDORS = ("DataDome", "Cloudflare", "Akamai", "PerimeterX", "unknown")
|
|
13
|
+
STEPS = ("posting", "form", "upload")
|
|
14
|
+
STATES = ("mapped", "dry_run_verified", "send_verified")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Verification:
|
|
19
|
+
date: date
|
|
20
|
+
version: str
|
|
21
|
+
by: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Block:
|
|
26
|
+
vendor: str
|
|
27
|
+
signal: str
|
|
28
|
+
step: str
|
|
29
|
+
date: date
|
|
30
|
+
version: str
|
|
31
|
+
camoufox: str
|
|
32
|
+
browser: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class System:
|
|
37
|
+
source: str
|
|
38
|
+
kind: str
|
|
39
|
+
account: str
|
|
40
|
+
hosting: tuple[str, ...]
|
|
41
|
+
adapter: str | None = None
|
|
42
|
+
share: float | None = None
|
|
43
|
+
block: Block | None = None
|
|
44
|
+
mapped: Verification | None = None
|
|
45
|
+
dry_run_verified: Verification | None = None
|
|
46
|
+
send_verified: Verification | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def state(self) -> str | None:
|
|
50
|
+
"""The furthest state reached; each one implies the ones before it."""
|
|
51
|
+
return next((s for s in reversed(STATES) if getattr(self, s)), None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _one_of(source: str, key: str, value: object, allowed: tuple[str, ...]) -> None:
|
|
55
|
+
if value not in allowed:
|
|
56
|
+
raise ValueError(f"{source}: {key} = {value!r}, expected one of {', '.join(allowed)}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _system(source: str, entry: dict) -> System:
|
|
60
|
+
known = {f.name for f in fields(System)} - {"source"}
|
|
61
|
+
if unknown := entry.keys() - known:
|
|
62
|
+
raise ValueError(f"{source}: unknown keys {', '.join(sorted(unknown))}")
|
|
63
|
+
_one_of(source, "kind", entry.get("kind"), KINDS)
|
|
64
|
+
_one_of(source, "account", entry.get("account"), ACCOUNTS)
|
|
65
|
+
for h in entry.get("hosting", []):
|
|
66
|
+
_one_of(source, "hosting", h, HOSTING)
|
|
67
|
+
block = entry.get("block")
|
|
68
|
+
if block is not None:
|
|
69
|
+
_one_of(source, "block.vendor", block.get("vendor"), VENDORS)
|
|
70
|
+
_one_of(source, "block.step", block.get("step"), STEPS)
|
|
71
|
+
block = Block(**block)
|
|
72
|
+
states = {s: Verification(**entry[s]) for s in STATES if s in entry}
|
|
73
|
+
return System(
|
|
74
|
+
source,
|
|
75
|
+
entry["kind"],
|
|
76
|
+
entry["account"],
|
|
77
|
+
tuple(entry.get("hosting", [])),
|
|
78
|
+
entry.get("adapter"),
|
|
79
|
+
entry.get("share"),
|
|
80
|
+
block,
|
|
81
|
+
**states,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse(text: str) -> dict[str, System]:
|
|
86
|
+
return {source: _system(source, entry) for source, entry in tomllib.loads(text).items()}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cache
|
|
90
|
+
def systems() -> dict[str, System]:
|
|
91
|
+
return parse(files("atsfill").joinpath("registry.toml").read_text())
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def with_state(text: str, source: str, state: str, v: Verification) -> str:
|
|
95
|
+
"""The registry file's text with `state` of `source` set to `v`, the rest untouched."""
|
|
96
|
+
lines = text.splitlines()
|
|
97
|
+
start = lines.index(f"[{source}]")
|
|
98
|
+
end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("[")), len(lines))
|
|
99
|
+
while lines[end - 1] == "":
|
|
100
|
+
end -= 1
|
|
101
|
+
entry = f'{state} = {{ date = {v.date}, version = "{v.version}", by = "{v.by}" }}'
|
|
102
|
+
at = next((i for i in range(start, end) if lines[i].startswith(f"{state} =")), None)
|
|
103
|
+
if at is None:
|
|
104
|
+
lines.insert(end, entry)
|
|
105
|
+
else:
|
|
106
|
+
lines[at] = entry
|
|
107
|
+
return "\n".join(lines) + "\n"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def for_adapter(name: str) -> System | None:
|
|
111
|
+
return next((s for s in systems().values() if s.adapter == name), None)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _backlog_key(s: System) -> tuple:
|
|
115
|
+
return (s.block is not None, ACCOUNTS.index(s.account), -(s.share or 0), s.source)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _row(s: System) -> str:
|
|
119
|
+
share = f"{s.share:.1%}" if s.share is not None else ""
|
|
120
|
+
block = f"{s.block.vendor} at {s.block.step}" if s.block else ""
|
|
121
|
+
state = s.state or ""
|
|
122
|
+
if s.adapter:
|
|
123
|
+
state = f"[{state}]({s.adapter}.md)" if state else f"[adapter]({s.adapter}.md)"
|
|
124
|
+
cells = [s.source, s.kind, share, s.account, ", ".join(s.hosting), block, state]
|
|
125
|
+
return "| " + " | ".join(cells) + " |"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def backlog(registry: dict[str, System] | None = None) -> str:
|
|
129
|
+
"""`docs/adapters/README.md`: the systems in backlog order (ADR 0013)."""
|
|
130
|
+
registry = systems() if registry is None else registry
|
|
131
|
+
head = "| Source | Kind | Share | Account | Hosting | Block | State |\n" + "|---" * 7 + "|"
|
|
132
|
+
adapters = sorted(
|
|
133
|
+
(s for s in registry.values() if s.kind in ("ats", "board")), key=_backlog_key
|
|
134
|
+
)
|
|
135
|
+
others = sorted(
|
|
136
|
+
(s for s in registry.values() if s.kind not in ("ats", "board")), key=_backlog_key
|
|
137
|
+
)
|
|
138
|
+
text = [
|
|
139
|
+
"# Hiring systems",
|
|
140
|
+
"",
|
|
141
|
+
"Generated from [src/atsfill/registry.toml](../../src/atsfill/registry.toml) by"
|
|
142
|
+
" `uv run scripts/backlog.py`. The registry is described in"
|
|
143
|
+
" [ADR 0012](../adr/0012-systems-registry.md), the order in"
|
|
144
|
+
" [ADR 0013](../adr/0013-survey-and-adapter-rounds.md): no account first, blocked last,"
|
|
145
|
+
" then by share of postings.",
|
|
146
|
+
"",
|
|
147
|
+
head,
|
|
148
|
+
*map(_row, adapters),
|
|
149
|
+
]
|
|
150
|
+
if others:
|
|
151
|
+
text += ["", "Aggregators and company-built forms, counted and not adapted:", "", head]
|
|
152
|
+
text += map(_row, others)
|
|
153
|
+
return "\n".join(text) + "\n"
|
|
@@ -61,6 +61,7 @@ class Result:
|
|
|
61
61
|
checks: list[Check] = field(default_factory=list)
|
|
62
62
|
side_effects: list[SideEffect] = field(default_factory=list)
|
|
63
63
|
evidence: str | None = None
|
|
64
|
+
warning: str | None = None # `code: detail`, on a send that went ahead anyway
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
@dataclass
|
|
@@ -69,6 +70,8 @@ class AdapterState:
|
|
|
69
70
|
submit_enabled: bool
|
|
70
71
|
side_effects: list[SideEffect]
|
|
71
72
|
side_effects_acknowledged: bool
|
|
73
|
+
verified: str | None = None # mapped | dry_run_verified | send_verified (ADR 0012)
|
|
74
|
+
verified_version: str | None = None
|
|
72
75
|
session: str | None = None # signed_in | signed_out | missing, for a system with accounts
|
|
73
76
|
|
|
74
77
|
|
|
@@ -77,7 +80,8 @@ class StatusReport:
|
|
|
77
80
|
version: str
|
|
78
81
|
data_dir: Path
|
|
79
82
|
config: Path
|
|
80
|
-
browser: str
|
|
83
|
+
browser: str # the installed build
|
|
84
|
+
browser_pinned: str
|
|
81
85
|
adapters: list[AdapterState]
|
|
82
86
|
|
|
83
87
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# The hiring systems atsfill has mapped (ADR 0012), one table per freehire `source`.
|
|
2
|
+
# `docs/adapters/README.md` is generated from this file: `uv run scripts/backlog.py`.
|
|
3
|
+
#
|
|
4
|
+
# kind ats | board | aggregator | custom
|
|
5
|
+
# share fraction of freehire's postings, worldwide, from its `source` facet
|
|
6
|
+
# account none | to_send | to_read
|
|
7
|
+
# hosting hosted, embedded: where the form is found
|
|
8
|
+
# adapter the adapter's name, when there is one
|
|
9
|
+
# block vendor, signal, step (posting | form | upload), date, version, camoufox, browser
|
|
10
|
+
# mapped, dry_run_verified, send_verified
|
|
11
|
+
# date, version, by; only the user sets send_verified
|
|
12
|
+
|
|
13
|
+
[ashby]
|
|
14
|
+
kind = "ats"
|
|
15
|
+
account = "none"
|
|
16
|
+
hosting = ["hosted", "embedded"]
|
|
17
|
+
adapter = "ashby"
|
|
18
|
+
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
19
|
+
|
|
20
|
+
[teamtailor]
|
|
21
|
+
kind = "ats"
|
|
22
|
+
account = "none"
|
|
23
|
+
hosting = ["hosted"]
|
|
24
|
+
adapter = "teamtailor"
|
|
25
|
+
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
26
|
+
|
|
27
|
+
[greenhouse]
|
|
28
|
+
kind = "ats"
|
|
29
|
+
account = "none"
|
|
30
|
+
hosting = ["hosted", "embedded"]
|
|
31
|
+
adapter = "greenhouse"
|
|
32
|
+
send_verified = { date = 2026-10-18, version = "0.0.2", by = "shbernal" }
|
|
33
|
+
|
|
34
|
+
[wttj]
|
|
35
|
+
kind = "board"
|
|
36
|
+
account = "to_send"
|
|
37
|
+
hosting = ["hosted"]
|
|
38
|
+
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
|