hh-applicant-tool 0.7.10__py3-none-any.whl → 1.4.12__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.
- hh_applicant_tool/__init__.py +1 -0
- hh_applicant_tool/__main__.py +1 -1
- hh_applicant_tool/ai/base.py +2 -0
- hh_applicant_tool/ai/openai.py +25 -35
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +65 -68
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -6
- hh_applicant_tool/api/datatypes.py +293 -0
- hh_applicant_tool/api/errors.py +57 -7
- hh_applicant_tool/api/user_agent.py +17 -0
- hh_applicant_tool/main.py +234 -113
- hh_applicant_tool/operations/apply_similar.py +353 -371
- hh_applicant_tool/operations/authorize.py +313 -120
- hh_applicant_tool/operations/call_api.py +18 -8
- hh_applicant_tool/operations/check_proxy.py +30 -0
- hh_applicant_tool/operations/clear_negotiations.py +90 -82
- hh_applicant_tool/operations/config.py +119 -16
- hh_applicant_tool/operations/install.py +34 -0
- hh_applicant_tool/operations/list_resumes.py +23 -11
- hh_applicant_tool/operations/log.py +77 -0
- hh_applicant_tool/operations/migrate_db.py +65 -0
- hh_applicant_tool/operations/query.py +122 -0
- hh_applicant_tool/operations/refresh_token.py +14 -13
- hh_applicant_tool/operations/reply_employers.py +201 -180
- hh_applicant_tool/operations/settings.py +95 -0
- hh_applicant_tool/operations/uninstall.py +26 -0
- hh_applicant_tool/operations/update_resumes.py +23 -11
- hh_applicant_tool/operations/whoami.py +40 -7
- hh_applicant_tool/storage/__init__.py +8 -0
- hh_applicant_tool/storage/facade.py +24 -0
- hh_applicant_tool/storage/models/__init__.py +0 -0
- hh_applicant_tool/storage/models/base.py +169 -0
- hh_applicant_tool/storage/models/contacts.py +28 -0
- hh_applicant_tool/storage/models/employer.py +12 -0
- hh_applicant_tool/storage/models/negotiation.py +16 -0
- hh_applicant_tool/storage/models/resume.py +19 -0
- hh_applicant_tool/storage/models/setting.py +6 -0
- hh_applicant_tool/storage/models/vacancy.py +36 -0
- hh_applicant_tool/storage/queries/migrations/.gitkeep +0 -0
- hh_applicant_tool/storage/queries/schema.sql +132 -0
- hh_applicant_tool/storage/repositories/__init__.py +0 -0
- hh_applicant_tool/storage/repositories/base.py +230 -0
- hh_applicant_tool/storage/repositories/contacts.py +14 -0
- hh_applicant_tool/storage/repositories/employers.py +14 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +13 -0
- hh_applicant_tool/storage/repositories/resumes.py +9 -0
- hh_applicant_tool/storage/repositories/settings.py +35 -0
- hh_applicant_tool/storage/repositories/vacancies.py +9 -0
- hh_applicant_tool/storage/utils.py +40 -0
- hh_applicant_tool/utils/__init__.py +31 -0
- hh_applicant_tool/utils/attrdict.py +6 -0
- hh_applicant_tool/utils/binpack.py +167 -0
- hh_applicant_tool/utils/config.py +55 -0
- hh_applicant_tool/utils/date.py +19 -0
- hh_applicant_tool/utils/json.py +61 -0
- hh_applicant_tool/{jsonc.py → utils/jsonc.py} +12 -6
- hh_applicant_tool/utils/log.py +147 -0
- hh_applicant_tool/utils/misc.py +12 -0
- hh_applicant_tool/utils/mixins.py +221 -0
- hh_applicant_tool/utils/string.py +27 -0
- hh_applicant_tool/utils/terminal.py +32 -0
- hh_applicant_tool-1.4.12.dist-info/METADATA +685 -0
- hh_applicant_tool-1.4.12.dist-info/RECORD +68 -0
- hh_applicant_tool/ai/blackbox.py +0 -55
- hh_applicant_tool/color_log.py +0 -47
- hh_applicant_tool/mixins.py +0 -13
- hh_applicant_tool/operations/delete_telemetry.py +0 -30
- hh_applicant_tool/operations/get_employer_contacts.py +0 -348
- hh_applicant_tool/telemetry_client.py +0 -106
- hh_applicant_tool/types.py +0 -45
- hh_applicant_tool/utils.py +0 -119
- hh_applicant_tool-0.7.10.dist-info/METADATA +0 -452
- hh_applicant_tool-0.7.10.dist-info/RECORD +0 -33
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/WHEEL +0 -0
- {hh_applicant_tool-0.7.10.dist-info → hh_applicant_tool-1.4.12.dist-info}/entry_points.txt +0 -0
|
@@ -1,109 +1,117 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import argparse
|
|
4
|
+
import datetime as dt
|
|
3
5
|
import logging
|
|
4
|
-
from
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..api.errors import ApiError
|
|
9
|
+
from ..main import BaseNamespace, BaseOperation
|
|
10
|
+
from ..utils.date import parse_api_datetime
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
from ..
|
|
8
|
-
from ..main import BaseOperation
|
|
9
|
-
from ..main import Namespace as BaseNamespace
|
|
10
|
-
from ..types import ApiListResponse
|
|
11
|
-
from ..utils import print_err, truncate_string
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..main import HHApplicantTool
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__package__)
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class Namespace(BaseNamespace):
|
|
17
|
-
|
|
19
|
+
cleanup: bool
|
|
18
20
|
blacklist_discard: bool
|
|
19
|
-
|
|
21
|
+
older_than: int
|
|
22
|
+
dry_run: bool
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class Operation(BaseOperation):
|
|
23
|
-
"""
|
|
26
|
+
"""Удаляет отказы либо старые отклики."""
|
|
27
|
+
|
|
28
|
+
__aliases__ = ["clear-negotiations", "delete-negotiations"]
|
|
24
29
|
|
|
25
30
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
26
31
|
parser.add_argument(
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
"-b",
|
|
33
|
+
"--blacklist-discard",
|
|
34
|
+
"--blacklist",
|
|
35
|
+
action=argparse.BooleanOptionalAction,
|
|
36
|
+
help="Блокировать работодателя за отказ",
|
|
31
37
|
)
|
|
32
38
|
parser.add_argument(
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
"-o",
|
|
40
|
+
"--older-than",
|
|
41
|
+
type=int,
|
|
42
|
+
help="С флагом --clean удаляет любые отклики старше N дней",
|
|
36
43
|
)
|
|
37
44
|
parser.add_argument(
|
|
38
|
-
"
|
|
39
|
-
|
|
45
|
+
"-n",
|
|
46
|
+
"--dry-run",
|
|
40
47
|
action=argparse.BooleanOptionalAction,
|
|
48
|
+
help="Тестовый запуск без реального удаления",
|
|
41
49
|
)
|
|
42
50
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
51
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
52
|
+
self.tool = tool
|
|
53
|
+
self.args: Namespace = tool.args
|
|
54
|
+
self.clear()
|
|
55
|
+
|
|
56
|
+
def clear(self) -> None:
|
|
57
|
+
blacklisted = set(self.tool.get_blacklisted())
|
|
58
|
+
storage = self.tool.storage
|
|
59
|
+
for negotiation in self.tool.get_negotiations():
|
|
60
|
+
vacancy = negotiation["vacancy"]
|
|
61
|
+
|
|
62
|
+
# Если работодателя блокируют, то он превращается в null
|
|
63
|
+
# ХХ позволяет скрывать компанию, когда id нет, а вместо имени "Крупная российская компания"
|
|
64
|
+
# sqlite3.IntegrityError: NOT NULL constraint failed: negotiations.employer_id
|
|
65
|
+
# try:
|
|
66
|
+
# storage.negotiations.save(negotiation)
|
|
67
|
+
# except RepositoryError as e:
|
|
68
|
+
# logger.exception(e)
|
|
69
|
+
|
|
70
|
+
if self.args.older_than:
|
|
71
|
+
updated_at = parse_api_datetime(negotiation["updated_at"])
|
|
72
|
+
# А хз какую временную зону сайт возвращает
|
|
73
|
+
days_passed = (
|
|
74
|
+
dt.datetime.now(updated_at.tzinfo) - updated_at
|
|
75
|
+
).days
|
|
76
|
+
logger.debug(f"{days_passed = }")
|
|
77
|
+
if days_passed <= self.args.older_than:
|
|
78
|
+
continue
|
|
79
|
+
elif negotiation["state"]["id"] != "discard":
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
if not self.args.dry_run:
|
|
83
|
+
self.tool.api_client.delete(
|
|
84
|
+
f"/negotiations/active/{negotiation['id']}",
|
|
85
|
+
with_decline_message=True,
|
|
73
86
|
)
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
):
|
|
77
|
-
decline_allowed = item.get("decline_allowed") or False
|
|
78
|
-
r = api_client.delete(
|
|
79
|
-
f"/negotiations/active/{item['id']}",
|
|
80
|
-
with_decline_message=decline_allowed,
|
|
81
|
-
)
|
|
82
|
-
assert {} == r
|
|
83
|
-
vacancy = item["vacancy"]
|
|
87
|
+
|
|
84
88
|
print(
|
|
85
|
-
"
|
|
86
|
-
state["name"].lower(),
|
|
89
|
+
"🗑️ Отменили отклик на вакансию:",
|
|
87
90
|
vacancy["alternate_url"],
|
|
88
|
-
"
|
|
89
|
-
truncate_string(vacancy["name"]),
|
|
90
|
-
")",
|
|
91
|
+
vacancy["name"],
|
|
91
92
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
")",
|
|
93
|
+
|
|
94
|
+
employer = vacancy.get("employer", {})
|
|
95
|
+
employer_id = employer.get("id")
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
self.args.blacklist_discard
|
|
99
|
+
and employer
|
|
100
|
+
and employer_id
|
|
101
|
+
and employer_id not in blacklisted
|
|
102
|
+
):
|
|
103
|
+
if not self.args.dry_run:
|
|
104
|
+
self.tool.api_client.put(
|
|
105
|
+
f"/employers/blacklisted/{employer_id}"
|
|
106
106
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
blacklisted.add(employer_id)
|
|
108
|
+
|
|
109
|
+
print(
|
|
110
|
+
"🚫 Работодатель заблокирован:",
|
|
111
|
+
employer["name"],
|
|
112
|
+
employer["alternate_url"],
|
|
113
|
+
)
|
|
114
|
+
except ApiError as err:
|
|
115
|
+
logger.error(err)
|
|
116
|
+
|
|
117
|
+
print("✅ Удаление откликов завершено.")
|
|
@@ -1,49 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import argparse
|
|
4
|
+
import json
|
|
2
5
|
import logging
|
|
3
6
|
import os
|
|
7
|
+
import platform
|
|
4
8
|
import subprocess
|
|
5
|
-
from typing import Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
6
10
|
|
|
7
|
-
from ..main import BaseOperation
|
|
8
|
-
from ..main import Namespace as BaseNamespace
|
|
11
|
+
from ..main import BaseNamespace, BaseOperation
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..main import HHApplicantTool
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__package__)
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
class Namespace(BaseNamespace):
|
|
16
21
|
show_path: bool
|
|
17
22
|
key: str
|
|
23
|
+
set: list[str]
|
|
24
|
+
edit: bool
|
|
25
|
+
unset: str
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
def get_value(data: dict[str, Any], path: str) -> Any:
|
|
21
29
|
for key in path.split("."):
|
|
22
|
-
if
|
|
30
|
+
if isinstance(data, dict):
|
|
31
|
+
data = data.get(key)
|
|
32
|
+
else:
|
|
23
33
|
return None
|
|
24
|
-
data = data[key]
|
|
25
34
|
return data
|
|
26
35
|
|
|
27
36
|
|
|
37
|
+
def set_value(data: dict[str, Any], path: str, value: Any) -> None:
|
|
38
|
+
"""Устанавливает значение во вложенном словаре по ключу в виде строки."""
|
|
39
|
+
keys = path.split(".")
|
|
40
|
+
for key in keys[:-1]:
|
|
41
|
+
data = data.setdefault(key, {})
|
|
42
|
+
data[keys[-1]] = value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def del_value(data: dict[str, Any], path: str) -> None:
|
|
46
|
+
"""Удаляет значение из вложенного словаря по ключу в виде строки."""
|
|
47
|
+
keys = path.split(".")
|
|
48
|
+
for key in keys[:-1]:
|
|
49
|
+
if not isinstance(data, dict) or key not in data:
|
|
50
|
+
return False
|
|
51
|
+
data = data[key]
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
del data[keys[-1]]
|
|
55
|
+
return True
|
|
56
|
+
except KeyError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_scalar(value: str) -> bool | int | float | str:
|
|
61
|
+
if value == "null":
|
|
62
|
+
return None
|
|
63
|
+
if value in ["true", "false"]:
|
|
64
|
+
return "t" in value
|
|
65
|
+
try:
|
|
66
|
+
return float(value) if "." in value else int(value)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
28
71
|
class Operation(BaseOperation):
|
|
29
|
-
"""
|
|
72
|
+
"""
|
|
73
|
+
Операции с конфигурационным файлом.
|
|
74
|
+
По умолчанию выводит содержимое конфига.
|
|
75
|
+
"""
|
|
30
76
|
|
|
31
77
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
32
|
-
parser.
|
|
78
|
+
group = parser.add_mutually_exclusive_group()
|
|
79
|
+
group.add_argument(
|
|
80
|
+
"-e",
|
|
81
|
+
"--edit",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Открыть конфигурационный файл в редакторе",
|
|
84
|
+
)
|
|
85
|
+
group.add_argument(
|
|
86
|
+
"-k", "--key", help="Вывести отдельное значение из конфига"
|
|
87
|
+
)
|
|
88
|
+
group.add_argument(
|
|
89
|
+
"-s",
|
|
90
|
+
"--set",
|
|
91
|
+
nargs=2,
|
|
92
|
+
metavar=("KEY", "VALUE"),
|
|
93
|
+
help="Установить значение в конфиг, например, --set openai.model gpt-4o",
|
|
94
|
+
)
|
|
95
|
+
group.add_argument(
|
|
96
|
+
"-u", "--unset", metavar="KEY", help="Удалить ключ из конфига"
|
|
97
|
+
)
|
|
98
|
+
group.add_argument(
|
|
33
99
|
"-p",
|
|
34
100
|
"--show-path",
|
|
35
101
|
"--path",
|
|
36
|
-
action=
|
|
102
|
+
action="store_true",
|
|
37
103
|
help="Вывести полный путь к конфигу",
|
|
38
104
|
)
|
|
39
|
-
parser.add_argument("-k", "--key", help="Вывести отдельное значение из конфига")
|
|
40
105
|
|
|
41
|
-
def run(self,
|
|
106
|
+
def run(self, applicant_tool: HHApplicantTool) -> None:
|
|
107
|
+
args = applicant_tool.args
|
|
108
|
+
config = applicant_tool.config
|
|
109
|
+
if args.set:
|
|
110
|
+
key, value = args.set
|
|
111
|
+
set_value(config, key, parse_scalar(value))
|
|
112
|
+
config.save()
|
|
113
|
+
logger.info("Значение '%s' для ключа '%s' сохранено.", value, key)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if args.unset:
|
|
117
|
+
key = args.unset
|
|
118
|
+
if del_value(config, key):
|
|
119
|
+
config.save()
|
|
120
|
+
logger.info("Ключ '%s' удален из конфига.", key)
|
|
121
|
+
else:
|
|
122
|
+
logger.warning("Ключ '%s' не найден в конфиге.", key)
|
|
123
|
+
return
|
|
124
|
+
|
|
42
125
|
if args.key:
|
|
43
|
-
|
|
126
|
+
value = get_value(config, args.key)
|
|
127
|
+
if value is not None:
|
|
128
|
+
print(value)
|
|
44
129
|
return
|
|
45
|
-
|
|
130
|
+
|
|
131
|
+
config_path = str(config._config_path)
|
|
46
132
|
if args.show_path:
|
|
47
133
|
print(config_path)
|
|
48
|
-
|
|
49
|
-
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if args.edit:
|
|
137
|
+
self._open_editor(config_path)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Default action: show content
|
|
141
|
+
print(json.dumps(config, indent=2, ensure_ascii=False))
|
|
142
|
+
|
|
143
|
+
def _open_editor(self, filepath: str) -> None:
|
|
144
|
+
"""Открывает файл в редакторе по умолчанию в зависимости от ОС."""
|
|
145
|
+
match platform.system():
|
|
146
|
+
case "Windows":
|
|
147
|
+
os.startfile(filepath)
|
|
148
|
+
case "Darwin": # macOS
|
|
149
|
+
subprocess.run(["open", filepath], check=True)
|
|
150
|
+
case _: # Linux и остальные (аналог else)
|
|
151
|
+
editor = os.getenv("EDITOR", "xdg-open")
|
|
152
|
+
subprocess.run([editor, filepath], check=True)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from runpy import run_module
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from ..main import BaseNamespace, BaseOperation
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..main import HHApplicantTool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__package__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Namespace(BaseNamespace):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Operation(BaseOperation):
|
|
23
|
+
"""Установит Chromium и другие зависимости"""
|
|
24
|
+
|
|
25
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def run(self, applicant_tool: HHApplicantTool) -> None:
|
|
29
|
+
orig_argv = sys.argv
|
|
30
|
+
sys.argv = ["playwright", "install", "chromium"]
|
|
31
|
+
try:
|
|
32
|
+
run_module("playwright", run_name="__main__")
|
|
33
|
+
finally:
|
|
34
|
+
sys.argv = orig_argv
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import argparse
|
|
3
4
|
import logging
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
4
6
|
|
|
5
7
|
from prettytable import PrettyTable
|
|
6
8
|
|
|
7
|
-
from ..api import
|
|
8
|
-
from ..main import BaseOperation
|
|
9
|
-
from ..
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
from ..api.datatypes import PaginatedItems
|
|
10
|
+
from ..main import BaseNamespace, BaseOperation
|
|
11
|
+
from ..utils.string import shorten
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..api import datatypes
|
|
15
|
+
from ..main import HHApplicantTool
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
logger = logging.getLogger(__package__)
|
|
14
19
|
|
|
@@ -20,20 +25,27 @@ class Namespace(BaseNamespace):
|
|
|
20
25
|
class Operation(BaseOperation):
|
|
21
26
|
"""Список резюме"""
|
|
22
27
|
|
|
28
|
+
__aliases__ = ("ls-resumes", "resumes")
|
|
29
|
+
|
|
23
30
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
24
31
|
pass
|
|
25
32
|
|
|
26
|
-
def run(self,
|
|
27
|
-
resumes:
|
|
28
|
-
|
|
33
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
34
|
+
resumes: PaginatedItems[datatypes.Resume] = tool.get_resumes()
|
|
35
|
+
logger.debug(resumes)
|
|
36
|
+
tool.storage.resumes.save_batch(resumes)
|
|
37
|
+
|
|
38
|
+
t = PrettyTable(
|
|
39
|
+
field_names=["ID", "Название", "Статус"], align="l", valign="t"
|
|
40
|
+
)
|
|
29
41
|
t.add_rows(
|
|
30
42
|
[
|
|
31
43
|
(
|
|
32
44
|
x["id"],
|
|
33
|
-
|
|
45
|
+
shorten(x["title"]),
|
|
34
46
|
x["status"]["name"].title(),
|
|
35
47
|
)
|
|
36
|
-
for x in resumes
|
|
48
|
+
for x in resumes
|
|
37
49
|
]
|
|
38
50
|
)
|
|
39
51
|
print(t)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from ..main import BaseNamespace, BaseOperation
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..main import HHApplicantTool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__package__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Namespace(BaseNamespace):
|
|
21
|
+
follow: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Operation(BaseOperation):
|
|
25
|
+
"""Просмотр файла-лога"""
|
|
26
|
+
|
|
27
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
28
|
+
# Изменено на -F для соответствия стандарту less/tail
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"-f",
|
|
31
|
+
"--follow",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="Следить за файлом (режим follow, аналог less +F)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
37
|
+
log_path = tool.log_file
|
|
38
|
+
|
|
39
|
+
if not os.path.exists(log_path):
|
|
40
|
+
logger.error("Файл лога не найден: %s", log_path)
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
if sys.platform == "win32":
|
|
44
|
+
os.startfile(log_path)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
pager = os.getenv("PAGER", "less")
|
|
48
|
+
if not shutil.which(pager):
|
|
49
|
+
logger.error("Не найден просмотрщик '%s'", pager)
|
|
50
|
+
if pager == "less":
|
|
51
|
+
logger.error(
|
|
52
|
+
'Попробуйте установить less: "sudo apt install less" или "sudo yum install less"'
|
|
53
|
+
)
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
cmd = [pager]
|
|
57
|
+
|
|
58
|
+
if pager == "less":
|
|
59
|
+
# -R позволяет отображать цвета (ANSI codes)
|
|
60
|
+
# -S отключает перенос строк (удобно для логов)
|
|
61
|
+
cmd.extend(["-R", "-S"])
|
|
62
|
+
if tool.args.follow:
|
|
63
|
+
# В less режим слежения включается через команду +F
|
|
64
|
+
cmd.append("+F")
|
|
65
|
+
|
|
66
|
+
cmd.append(str(log_path))
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Запускаем процесс. check=False, так как выход из pager
|
|
70
|
+
# по Ctrl+C может вернуть ненулевой код.
|
|
71
|
+
subprocess.run(cmd, check=False)
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
logger.error("Не удалось запустить просмотрщик '%s'", pager)
|
|
74
|
+
return 1
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
# Обработка прерывания, чтобы не выводить traceback в консоль
|
|
77
|
+
pass
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from ..main import BaseNamespace, BaseOperation
|
|
11
|
+
from ..storage import apply_migration, list_migrations
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..main import HHApplicantTool
|
|
15
|
+
|
|
16
|
+
SUCKASS = "✅ Success!"
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__package__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Namespace(BaseNamespace):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Operation(BaseOperation):
|
|
26
|
+
"""Выполняет миграцию БД. Если первым аргументом имя миграции не передано, выведет их список.""" # noqa: E501
|
|
27
|
+
|
|
28
|
+
__aliases__: list[str] = ["migrate"]
|
|
29
|
+
|
|
30
|
+
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
31
|
+
parser.add_argument("name", nargs="?", help="Имя миграции")
|
|
32
|
+
|
|
33
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
34
|
+
def apply(name: str) -> None:
|
|
35
|
+
apply_migration(tool.db, name)
|
|
36
|
+
print(SUCKASS)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if a := tool.args.name:
|
|
40
|
+
return apply(a)
|
|
41
|
+
if not (migrations := list_migrations()):
|
|
42
|
+
return
|
|
43
|
+
if not sys.stdout.isatty():
|
|
44
|
+
print(*list_migrations(), sep=os.sep)
|
|
45
|
+
return
|
|
46
|
+
print("List of migrations:")
|
|
47
|
+
print()
|
|
48
|
+
for n, migration in enumerate(migrations, 1):
|
|
49
|
+
print(f" [{n}]: {migration}")
|
|
50
|
+
print()
|
|
51
|
+
L = len(migrations)
|
|
52
|
+
if n := int(
|
|
53
|
+
input(
|
|
54
|
+
f"Choose migration [1{f'-{L}' if L > 1 else ''}] (Keep empty to exit): " # noqa: E501
|
|
55
|
+
)
|
|
56
|
+
or 0
|
|
57
|
+
):
|
|
58
|
+
apply(migrations[n - 1])
|
|
59
|
+
except sqlite3.OperationalError as ex:
|
|
60
|
+
logger.exception(ex)
|
|
61
|
+
logger.warning(
|
|
62
|
+
f"Если ничего не помогает, то вы можете просто удалить базу, сделав бекап:\n\n"
|
|
63
|
+
f" $ mv {tool.db_path}{{,.bak}}"
|
|
64
|
+
)
|
|
65
|
+
return 1
|