atsfill 0.0.4.dev91__tar.gz → 0.0.4.dev92__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.
Files changed (46) hide show
  1. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/ashby.py +2 -2
  5. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/oracle.py +4 -3
  6. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/pinpoint.py +3 -1
  7. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/matching.py +14 -2
  8. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/results.py +1 -0
  9. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/LICENSE +0 -0
  10. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/README.md +0 -0
  11. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/__init__.py +0 -0
  12. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/__init__.py +0 -0
  13. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/bamboohr.py +0 -0
  14. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/breezy.py +0 -0
  15. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/dayforce.py +0 -0
  16. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/eightfold.py +0 -0
  17. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/greenhouse.py +0 -0
  18. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/hireology.py +0 -0
  19. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/hiringthing.py +0 -0
  20. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/jazzhr.py +0 -0
  21. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/lever.py +0 -0
  22. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/manatal.py +0 -0
  23. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/paycor.py +0 -0
  24. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/paylocity.py +0 -0
  25. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/personio.py +0 -0
  26. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/phenom.py +0 -0
  27. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/recruitee.py +0 -0
  28. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/rippling.py +0 -0
  29. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/smartrecruiters.py +0 -0
  30. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/teamtailor.py +0 -0
  31. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/workable.py +0 -0
  32. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/workday.py +0 -0
  33. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/wttj.py +0 -0
  34. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/adapters/zohorecruit.py +0 -0
  35. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/cli.py +0 -0
  36. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/__init__.py +0 -0
  37. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/accounts.py +0 -0
  38. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/adapter.py +0 -0
  39. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/attempts.py +0 -0
  40. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/browser.py +0 -0
  41. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/commands.py +0 -0
  42. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/passwords.py +0 -0
  43. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/paths.py +0 -0
  44. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/core/registry.py +0 -0
  45. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/py.typed +0 -0
  46. {atsfill-0.0.4.dev91 → atsfill-0.0.4.dev92}/src/atsfill/registry.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.4.dev91
3
+ Version: 0.0.4.dev92
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev91"
3
+ version = "0.0.4.dev92"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev91"
3
+ version = "0.0.4.dev92"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -34,7 +34,7 @@ TYPES = {
34
34
  "String": FieldType.TEXT,
35
35
  "Email": FieldType.TEXT,
36
36
  "LongText": FieldType.TEXT,
37
- "Url": FieldType.TEXT,
37
+ "Url": FieldType.URL,
38
38
  "Date": FieldType.DATE,
39
39
  "Number": FieldType.TEXT,
40
40
  "Phone": FieldType.PHONE,
@@ -146,7 +146,7 @@ class Ashby:
146
146
  entry = page.locator(f'[data-field-path="{field_id}"]')
147
147
  entry.scroll_into_view_if_needed()
148
148
  match fields[field_id].type, answer:
149
- case FieldType.TEXT | FieldType.DATE | FieldType.PHONE, str():
149
+ case FieldType.TEXT | FieldType.URL | FieldType.DATE | FieldType.PHONE, str():
150
150
  box = entry.locator("input, textarea").first
151
151
  box.fill(answer)
152
152
  if fields[field_id].type is FieldType.DATE:
@@ -46,6 +46,7 @@ UPLOAD_TIMEOUT_MS = 30_000
46
46
  CHECKBOX = ["Yes", "No"]
47
47
  KINDS = {
48
48
  "text": FieldType.TEXT,
49
+ "url": FieldType.URL,
49
50
  "phone": FieldType.PHONE,
50
51
  "date": FieldType.DATE,
51
52
  "pills": FieldType.CHOICE,
@@ -136,7 +137,7 @@ _READ = """() => {
136
137
  field.kind = "checks";
137
138
  field.options = pills(".apply-flow-input-checkbox__label");
138
139
  } else if (input?.matches("textarea, input.input-row__control") && !input.disabled) {
139
- field.kind = input.type === "email" ? "email" : "text";
140
+ field.kind = ["email", "url"].includes(input.type) ? input.type : "text";
140
141
  field.value = input.value;
141
142
  field.maxLength = input.maxLength > 0 ? input.maxLength : null;
142
143
  } else if (input?.disabled || !row.querySelector(CONTROL)) {
@@ -212,7 +213,7 @@ _READ_BACK = """(ids) => {
212
213
  const picked = [...row.querySelectorAll("input:checked")]
213
214
  .map(i => text(row.querySelector(`label[for="${i.id}"]`)));
214
215
  out[id] = kind === "radios" ? picked[0] ?? null : picked;
215
- } else if (kind === "text" || kind === "email") {
216
+ } else if (kind === "text" || kind === "email" || kind === "url") {
216
217
  out[id] = input.value;
217
218
  }
218
219
  }
@@ -431,7 +432,7 @@ class Oracle:
431
432
  return
432
433
  row = self._row(page, field_id)
433
434
  match kind, answer:
434
- case "text", str():
435
+ case "text" | "url", str():
435
436
  box = row.locator("input, textarea").first
436
437
  box.fill(answer)
437
438
  if box.get_attribute("role") == "combobox": # an address search, left unpicked
@@ -40,7 +40,7 @@ SUBMIT_TIMEOUT_MS = 90_000
40
40
  YES_NO = ["Yes", "No"]
41
41
  US = "United States" # the country whose address asks for a city and a state
42
42
  JOB_PREFERENCES = "job_preferences"
43
- TEXT_QUESTIONS = {"Shorttext", "Longtext", "Url", "Numberinput"}
43
+ TEXT_QUESTIONS = {"Shorttext", "Longtext", "Url", "Numberinput"} # Url is a `url` field
44
44
  FILE_QUESTIONS = {"Document", "Multipledocuments"}
45
45
  NOT_FIELDS = {"New", "Chatbot", "Stagescarousel"} # components without a field, by last name
46
46
  VOID = {"input", "br", "img", "hr", "meta", "link", "source", "wbr", "col", "area"}
@@ -459,6 +459,8 @@ def _component(
459
459
  yes_no = props.get("translations", {})
460
460
  if short in TEXT_QUESTIONS:
461
461
  field_type, spec = FieldType.TEXT, _Spec("text", props["name"], root)
462
+ if short == "Url":
463
+ field_type = FieldType.URL
462
464
  if short == "Numberinput":
463
465
  label += " (a number)"
464
466
  elif short == "Boolean":
@@ -3,6 +3,7 @@
3
3
  import re
4
4
  import unicodedata
5
5
  from pathlib import Path, PurePath
6
+ from urllib.parse import urlsplit
6
7
 
7
8
  from atsfill.core.results import Answer, Check, Field, FieldType
8
9
 
@@ -25,6 +26,11 @@ def _picked_from(answer: str, suggestion: str) -> bool:
25
26
  return all(any(p == wanted.strip() for p in parts) for wanted in answer.split(","))
26
27
 
27
28
 
29
+ def _absolute_url(value: str) -> bool:
30
+ parts = urlsplit(_norm(value))
31
+ return parts.scheme in ("http", "https") and bool(parts.netloc)
32
+
33
+
28
34
  def matches(field: Field, answer: Answer, read_back: Answer | None) -> bool:
29
35
  if read_back is None:
30
36
  return False
@@ -36,7 +42,7 @@ def matches(field: Field, answer: Answer, read_back: Answer | None) -> bool:
36
42
  if not isinstance(answer, str) or not isinstance(read_back, str):
37
43
  return False
38
44
  match field.type:
39
- case FieldType.TEXT:
45
+ case FieldType.TEXT | FieldType.URL:
40
46
  return _norm(answer) == _norm(read_back)
41
47
  case FieldType.DATE:
42
48
  return answer == read_back
@@ -74,8 +80,14 @@ def _answer_problem(field: Field, answer: object) -> str | None:
74
80
  if field.type in (FieldType.CHOICE, FieldType.MULTI_CHOICE) and not _in_options(field, values):
75
81
  return f"{answer!r} is not one of {field.options}"
76
82
  length = len(_norm(values[0]))
77
- if field.type is FieldType.TEXT and field.max_length and length > field.max_length:
83
+ if (
84
+ field.type in (FieldType.TEXT, FieldType.URL)
85
+ and field.max_length
86
+ and length > field.max_length
87
+ ):
78
88
  return f"{length} characters, over the limit of {field.max_length}"
89
+ if field.type is FieldType.URL and not _absolute_url(values[0]):
90
+ return "a url answer is an absolute http(s) URL"
79
91
  if field.type is FieldType.DATE and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", values[0]):
80
92
  return "a date answer is YYYY-MM-DD"
81
93
  if field.type is FieldType.FILE:
@@ -20,6 +20,7 @@ class FieldType(StrEnum):
20
20
  TYPEAHEAD = "typeahead"
21
21
  FILE = "file"
22
22
  PHONE = "phone"
23
+ URL = "url" # the system declares it a URL; answered as an absolute http(s) URL
23
24
 
24
25
 
25
26
  type Answer = str | list[str]
File without changes
File without changes