hh-applicant-tool 1.4.7__py3-none-any.whl → 1.5.7__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/__main__.py +1 -1
- hh_applicant_tool/ai/__init__.py +1 -0
- hh_applicant_tool/ai/openai.py +30 -14
- hh_applicant_tool/api/__init__.py +4 -2
- hh_applicant_tool/api/client.py +32 -17
- hh_applicant_tool/{constants.py → api/client_keys.py} +3 -3
- hh_applicant_tool/{datatypes.py → api/datatypes.py} +2 -0
- hh_applicant_tool/api/errors.py +8 -2
- hh_applicant_tool/{utils → api}/user_agent.py +1 -1
- hh_applicant_tool/main.py +63 -38
- hh_applicant_tool/operations/apply_similar.py +136 -52
- hh_applicant_tool/operations/authorize.py +97 -28
- hh_applicant_tool/operations/call_api.py +3 -3
- hh_applicant_tool/operations/{check_negotiations.py → clear_negotiations.py} +40 -26
- hh_applicant_tool/operations/list_resumes.py +5 -7
- hh_applicant_tool/operations/query.py +5 -3
- hh_applicant_tool/operations/refresh_token.py +9 -2
- hh_applicant_tool/operations/reply_employers.py +80 -40
- hh_applicant_tool/operations/settings.py +2 -2
- hh_applicant_tool/operations/update_resumes.py +5 -4
- hh_applicant_tool/operations/whoami.py +3 -3
- hh_applicant_tool/storage/__init__.py +5 -1
- hh_applicant_tool/storage/facade.py +2 -2
- hh_applicant_tool/storage/models/base.py +9 -4
- hh_applicant_tool/storage/models/contacts.py +42 -0
- hh_applicant_tool/storage/queries/schema.sql +23 -10
- hh_applicant_tool/storage/repositories/base.py +69 -15
- hh_applicant_tool/storage/repositories/contacts.py +5 -10
- hh_applicant_tool/storage/repositories/employers.py +1 -0
- hh_applicant_tool/storage/repositories/errors.py +19 -0
- hh_applicant_tool/storage/repositories/negotiations.py +1 -0
- hh_applicant_tool/storage/repositories/resumes.py +2 -7
- hh_applicant_tool/storage/repositories/settings.py +1 -0
- hh_applicant_tool/storage/repositories/vacancies.py +1 -0
- hh_applicant_tool/storage/utils.py +12 -15
- hh_applicant_tool/utils/__init__.py +3 -3
- hh_applicant_tool/utils/config.py +1 -1
- hh_applicant_tool/utils/log.py +6 -3
- hh_applicant_tool/utils/mixins.py +28 -46
- hh_applicant_tool/utils/string.py +15 -0
- hh_applicant_tool/utils/terminal.py +115 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/METADATA +384 -162
- hh_applicant_tool-1.5.7.dist-info/RECORD +68 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/WHEEL +1 -1
- hh_applicant_tool/storage/models/contact.py +0 -16
- hh_applicant_tool-1.4.7.dist-info/RECORD +0 -67
- /hh_applicant_tool/utils/{dateutil.py → date.py} +0 -0
- /hh_applicant_tool/utils/{jsonutil.py → json.py} +0 -0
- {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/entry_points.txt +0 -0
|
@@ -6,12 +6,12 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
from prettytable import PrettyTable
|
|
8
8
|
|
|
9
|
-
from ..datatypes import PaginatedItems
|
|
9
|
+
from ..api.datatypes import PaginatedItems
|
|
10
10
|
from ..main import BaseNamespace, BaseOperation
|
|
11
11
|
from ..utils.string import shorten
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from .. import datatypes
|
|
14
|
+
from ..api import datatypes
|
|
15
15
|
from ..main import HHApplicantTool
|
|
16
16
|
|
|
17
17
|
|
|
@@ -32,9 +32,8 @@ class Operation(BaseOperation):
|
|
|
32
32
|
|
|
33
33
|
def run(self, tool: HHApplicantTool) -> None:
|
|
34
34
|
resumes: PaginatedItems[datatypes.Resume] = tool.get_resumes()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
storage.resumes.save(resume)
|
|
35
|
+
logger.debug(resumes)
|
|
36
|
+
tool.storage.resumes.save_batch(resumes)
|
|
38
37
|
|
|
39
38
|
t = PrettyTable(
|
|
40
39
|
field_names=["ID", "Название", "Статус"], align="l", valign="t"
|
|
@@ -46,8 +45,7 @@ class Operation(BaseOperation):
|
|
|
46
45
|
shorten(x["title"]),
|
|
47
46
|
x["status"]["name"].title(),
|
|
48
47
|
)
|
|
49
|
-
for x in resumes
|
|
48
|
+
for x in resumes
|
|
50
49
|
]
|
|
51
50
|
)
|
|
52
51
|
print(t)
|
|
53
|
-
print(f"\nНайдено резюме: {resumes['found']}")
|
|
@@ -68,7 +68,7 @@ class Operation(BaseOperation):
|
|
|
68
68
|
writer.writerows(cursor.fetchall())
|
|
69
69
|
|
|
70
70
|
if tool.args.output:
|
|
71
|
-
print(f"✅
|
|
71
|
+
print(f"✅ Exported to {tool.args.output.name}")
|
|
72
72
|
return
|
|
73
73
|
|
|
74
74
|
rows = cursor.fetchmany(MAX_RESULTS + 1)
|
|
@@ -88,10 +88,12 @@ class Operation(BaseOperation):
|
|
|
88
88
|
)
|
|
89
89
|
else:
|
|
90
90
|
tool.db.commit()
|
|
91
|
-
|
|
91
|
+
|
|
92
|
+
if cursor.rowcount > 0:
|
|
93
|
+
print(f"Rows affected: {cursor.rowcount}")
|
|
92
94
|
|
|
93
95
|
except sqlite3.Error as ex:
|
|
94
|
-
print(f"❌
|
|
96
|
+
print(f"❌ SQL Error: {ex}")
|
|
95
97
|
return 1
|
|
96
98
|
|
|
97
99
|
if initial_sql := tool.args.sql:
|
|
@@ -26,5 +26,12 @@ class Operation(BaseOperation):
|
|
|
26
26
|
pass
|
|
27
27
|
|
|
28
28
|
def run(self, tool: HHApplicantTool) -> None:
|
|
29
|
-
tool.api_client.
|
|
30
|
-
|
|
29
|
+
if tool.api_client.is_access_expired:
|
|
30
|
+
tool.api_client.refresh_access_token()
|
|
31
|
+
if not tool.save_token():
|
|
32
|
+
print("⚠️ Токен не был обновлен!")
|
|
33
|
+
return 1
|
|
34
|
+
print("✅ Токен успешно обновлен.")
|
|
35
|
+
else:
|
|
36
|
+
# logger.debug("Токен валиден, игнорируем обновление.")
|
|
37
|
+
print("ℹ️ Токен не истек, обновление не требуется.")
|
|
@@ -3,13 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
import logging
|
|
5
5
|
import random
|
|
6
|
+
from datetime import datetime
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
|
-
from .. import datatypes
|
|
9
9
|
from ..ai.base import AIError
|
|
10
|
-
from ..api import ApiError
|
|
10
|
+
from ..api import ApiError, datatypes
|
|
11
11
|
from ..main import BaseNamespace, BaseOperation
|
|
12
|
-
from ..utils.
|
|
12
|
+
from ..utils.date import parse_api_datetime
|
|
13
13
|
from ..utils.string import rand_text
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
@@ -37,21 +37,30 @@ class Namespace(BaseNamespace):
|
|
|
37
37
|
use_ai: bool
|
|
38
38
|
first_prompt: str
|
|
39
39
|
prompt: str
|
|
40
|
+
period: int
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
class Operation(BaseOperation):
|
|
43
44
|
"""Ответ всем работодателям."""
|
|
44
45
|
|
|
45
|
-
__aliases__ = ["reply-
|
|
46
|
+
__aliases__ = ["reply-empls", "reply-chats", "reall"]
|
|
46
47
|
|
|
47
48
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
48
|
-
parser.add_argument(
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--resume-id",
|
|
51
|
+
help="Идентификатор резюме. Если не указан, то просматриваем чаты для всех резюме",
|
|
52
|
+
)
|
|
49
53
|
parser.add_argument(
|
|
50
54
|
"-m",
|
|
51
55
|
"--reply-message",
|
|
52
56
|
"--reply",
|
|
53
57
|
help="Отправить сообщение во все чаты. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.", # noqa: E501
|
|
54
58
|
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--period",
|
|
61
|
+
type=int,
|
|
62
|
+
help="Игнорировать отклики, которые не обновлялись больше N дней",
|
|
63
|
+
)
|
|
55
64
|
parser.add_argument(
|
|
56
65
|
"-p",
|
|
57
66
|
"--max-pages",
|
|
@@ -92,7 +101,7 @@ class Operation(BaseOperation):
|
|
|
92
101
|
|
|
93
102
|
def run(self, tool: HHApplicantTool) -> None:
|
|
94
103
|
args: Namespace = tool.args
|
|
95
|
-
self.
|
|
104
|
+
self.tool = tool
|
|
96
105
|
self.api_client = tool.api_client
|
|
97
106
|
self.resume_id = tool.first_resume_id()
|
|
98
107
|
self.reply_message = args.reply_message or tool.config.get(
|
|
@@ -106,27 +115,60 @@ class Operation(BaseOperation):
|
|
|
106
115
|
self.openai_chat = (
|
|
107
116
|
tool.get_openai_chat(args.first_prompt) if args.use_ai else None
|
|
108
117
|
)
|
|
118
|
+
self.period = args.period
|
|
109
119
|
|
|
110
120
|
logger.debug(f"{self.reply_message = }")
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
self.reply_employers()
|
|
122
|
+
|
|
123
|
+
def reply_employers(self):
|
|
124
|
+
blacklist = set(self.tool.get_blacklisted())
|
|
125
|
+
me: datatypes.User = self.tool.get_me()
|
|
126
|
+
resumes = self.tool.get_resumes()
|
|
127
|
+
resumes = (
|
|
128
|
+
list(filter(lambda x: x["id"] == self.resume_id, resumes))
|
|
129
|
+
if self.resume_id
|
|
130
|
+
else resumes
|
|
131
|
+
)
|
|
132
|
+
resumes = list(
|
|
133
|
+
filter(
|
|
134
|
+
lambda resume: resume["status"]["id"] == "published", resumes
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
self._reply_chats(user=me, resumes=resumes, blacklist=blacklist)
|
|
138
|
+
|
|
139
|
+
def _reply_chats(
|
|
140
|
+
self,
|
|
141
|
+
user: datatypes.User,
|
|
142
|
+
resumes: list[datatypes.Resume],
|
|
143
|
+
blacklist: set[str],
|
|
144
|
+
) -> None:
|
|
145
|
+
resume_map = {r["id"]: r for r in resumes}
|
|
146
|
+
|
|
147
|
+
base_placeholders = {
|
|
148
|
+
"first_name": user.get("first_name") or "",
|
|
149
|
+
"last_name": user.get("last_name") or "",
|
|
150
|
+
"email": user.get("email") or "",
|
|
151
|
+
"phone": user.get("phone") or "",
|
|
123
152
|
}
|
|
124
153
|
|
|
125
|
-
for negotiation in self.
|
|
154
|
+
for negotiation in self.tool.get_negotiations():
|
|
126
155
|
try:
|
|
127
|
-
|
|
156
|
+
# try:
|
|
157
|
+
# self.tool.storage.negotiations.save(negotiation)
|
|
158
|
+
# except RepositoryError as e:
|
|
159
|
+
# logger.exception(e)
|
|
128
160
|
|
|
129
|
-
if
|
|
161
|
+
if not (resume := resume_map.get(negotiation["resume"]["id"])):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
updated_at = parse_api_datetime(negotiation["updated_at"])
|
|
165
|
+
|
|
166
|
+
# Пропуск откликов, которые не обновлялись более N дней (при просмотре они обновляются вроде)
|
|
167
|
+
if (
|
|
168
|
+
self.period
|
|
169
|
+
and (datetime().now(updated_at.tzinfo) - updated_at).days
|
|
170
|
+
> self.period
|
|
171
|
+
):
|
|
130
172
|
continue
|
|
131
173
|
|
|
132
174
|
state_id = negotiation["state"]["id"]
|
|
@@ -141,26 +183,27 @@ class Operation(BaseOperation):
|
|
|
141
183
|
employer = vacancy.get("employer") or {}
|
|
142
184
|
salary = vacancy.get("salary") or {}
|
|
143
185
|
|
|
144
|
-
if employer.get("id") in
|
|
186
|
+
if employer.get("id") in blacklist:
|
|
145
187
|
print(
|
|
146
188
|
"🚫 Пропускаем заблокированного работодателя",
|
|
147
189
|
employer.get("alternate_url"),
|
|
148
190
|
)
|
|
149
191
|
continue
|
|
150
192
|
|
|
151
|
-
|
|
193
|
+
placeholders = {
|
|
152
194
|
"vacancy_name": vacancy.get("name", ""),
|
|
153
195
|
"employer_name": employer.get("name", ""),
|
|
154
|
-
|
|
196
|
+
"resume_title": resume.get("title") or "",
|
|
197
|
+
**base_placeholders,
|
|
155
198
|
}
|
|
156
199
|
|
|
157
200
|
logger.debug(
|
|
158
201
|
"Вакансия %(vacancy_name)s от %(employer_name)s"
|
|
159
|
-
%
|
|
202
|
+
% placeholders
|
|
160
203
|
)
|
|
161
204
|
|
|
162
205
|
page: int = 0
|
|
163
|
-
last_message:
|
|
206
|
+
last_message: datatypes.Message | None = None
|
|
164
207
|
message_history: list[str] = []
|
|
165
208
|
while True:
|
|
166
209
|
messages_res: datatypes.PaginatedItems[
|
|
@@ -183,7 +226,8 @@ class Operation(BaseOperation):
|
|
|
183
226
|
)
|
|
184
227
|
message_date = parse_api_datetime(
|
|
185
228
|
message.get("created_at")
|
|
186
|
-
).
|
|
229
|
+
).strftime("%d.%m.%Y %H:%M:%S")
|
|
230
|
+
|
|
187
231
|
message_history.append(
|
|
188
232
|
f"[ {message_date} ] {author}: {message['text']}"
|
|
189
233
|
)
|
|
@@ -205,13 +249,13 @@ class Operation(BaseOperation):
|
|
|
205
249
|
send_message = ""
|
|
206
250
|
if self.reply_message:
|
|
207
251
|
send_message = (
|
|
208
|
-
rand_text(self.reply_message) %
|
|
252
|
+
rand_text(self.reply_message) % placeholders
|
|
209
253
|
)
|
|
210
254
|
logger.debug(f"Template message: {send_message}")
|
|
211
255
|
elif self.openai_chat:
|
|
212
256
|
try:
|
|
213
257
|
ai_query = (
|
|
214
|
-
f"Вакансия: {
|
|
258
|
+
f"Вакансия: {placeholders['vacancy_name']}\n"
|
|
215
259
|
f"История переписки:\n"
|
|
216
260
|
+ "\n".join(message_history[-10:])
|
|
217
261
|
+ f"\n\nИнструкция: {self.pre_prompt}"
|
|
@@ -226,12 +270,8 @@ class Operation(BaseOperation):
|
|
|
226
270
|
)
|
|
227
271
|
continue
|
|
228
272
|
else:
|
|
229
|
-
print(
|
|
230
|
-
|
|
231
|
-
message_placeholders["employer_name"],
|
|
232
|
-
"| 💼",
|
|
233
|
-
message_placeholders["vacancy_name"],
|
|
234
|
-
)
|
|
273
|
+
print("🏢", placeholders["employer_name"])
|
|
274
|
+
print("💼", placeholders["vacancy_name"])
|
|
235
275
|
if salary:
|
|
236
276
|
print(
|
|
237
277
|
"💵 от",
|
|
@@ -251,9 +291,10 @@ class Operation(BaseOperation):
|
|
|
251
291
|
print(msg)
|
|
252
292
|
|
|
253
293
|
try:
|
|
254
|
-
print("-" *
|
|
294
|
+
print("-" * 40)
|
|
295
|
+
print("Активное резюме:", resume.get("title") or "")
|
|
255
296
|
print(
|
|
256
|
-
"
|
|
297
|
+
"/ban, /cancel необязательное сообщение для отмены"
|
|
257
298
|
)
|
|
258
299
|
send_message = input("Ваше сообщение: ").strip()
|
|
259
300
|
except EOFError:
|
|
@@ -264,13 +305,12 @@ class Operation(BaseOperation):
|
|
|
264
305
|
continue
|
|
265
306
|
|
|
266
307
|
if send_message.startswith("/ban"):
|
|
267
|
-
self.applicant_tool.storage.employers.save(employer)
|
|
268
308
|
self.api_client.put(
|
|
269
309
|
f"/employers/blacklisted/{employer['id']}"
|
|
270
310
|
)
|
|
271
|
-
|
|
311
|
+
blacklist.add(employer["id"])
|
|
272
312
|
print(
|
|
273
|
-
"🚫 Работодатель
|
|
313
|
+
"🚫 Работодатель заблокирован",
|
|
274
314
|
employer.get("alternate_url"),
|
|
275
315
|
)
|
|
276
316
|
continue
|
|
@@ -7,8 +7,8 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
from prettytable import PrettyTable
|
|
9
9
|
|
|
10
|
+
from .. import utils
|
|
10
11
|
from ..main import BaseNamespace, BaseOperation
|
|
11
|
-
from ..utils import jsonutil
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from ..main import HHApplicantTool
|
|
@@ -28,7 +28,7 @@ class Namespace(BaseNamespace):
|
|
|
28
28
|
|
|
29
29
|
def parse_value(v):
|
|
30
30
|
try:
|
|
31
|
-
return
|
|
31
|
+
return utils.json.loads(v)
|
|
32
32
|
except json.JSONDecodeError:
|
|
33
33
|
return v
|
|
34
34
|
|
|
@@ -5,8 +5,7 @@ import argparse
|
|
|
5
5
|
import logging
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from ..api import ApiError
|
|
9
|
-
from ..datatypes import PaginatedItems
|
|
8
|
+
from ..api import ApiError, datatypes
|
|
10
9
|
from ..main import BaseNamespace, BaseOperation
|
|
11
10
|
from ..utils import print_err
|
|
12
11
|
from ..utils.string import shorten
|
|
@@ -31,8 +30,10 @@ class Operation(BaseOperation):
|
|
|
31
30
|
pass
|
|
32
31
|
|
|
33
32
|
def run(self, tool: HHApplicantTool) -> None:
|
|
34
|
-
resumes:
|
|
35
|
-
|
|
33
|
+
resumes: list[datatypes.Resume] = tool.get_resumes()
|
|
34
|
+
# Там вызов API меняет поля
|
|
35
|
+
# tool.storage.resumes.save_batch(resumes)
|
|
36
|
+
for resume in resumes:
|
|
36
37
|
try:
|
|
37
38
|
res = tool.api_client.post(
|
|
38
39
|
f"/resumes/{resume['id']}/publish",
|
|
@@ -5,7 +5,7 @@ import argparse
|
|
|
5
5
|
import logging
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from .. import datatypes
|
|
8
|
+
from ..api import datatypes
|
|
9
9
|
from ..main import BaseNamespace, BaseOperation
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
@@ -53,6 +53,6 @@ class Operation(BaseOperation):
|
|
|
53
53
|
print(
|
|
54
54
|
f"🆔 {result['id']} {full_name or 'Анонимный аккаунт'} "
|
|
55
55
|
f"[ 📄 {counters['resumes_count']} "
|
|
56
|
-
f"| 👁️
|
|
57
|
-
f"| ✉️
|
|
56
|
+
f"| 👁️ {fmt_plus(counters['new_resume_views'])} "
|
|
57
|
+
f"| ✉️ {fmt_plus(counters['unread_negotiations'])} ]"
|
|
58
58
|
)
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import sqlite3
|
|
4
4
|
|
|
5
|
-
from .repositories.contacts import
|
|
5
|
+
from .repositories.contacts import VacancyContactsRepository
|
|
6
6
|
from .repositories.employers import EmployersRepository
|
|
7
7
|
from .repositories.negotiations import NegotiationRepository
|
|
8
8
|
from .repositories.resumes import ResumesRepository
|
|
@@ -18,7 +18,7 @@ class StorageFacade:
|
|
|
18
18
|
init_db(conn)
|
|
19
19
|
self.employers = EmployersRepository(conn)
|
|
20
20
|
self.vacancies = VacanciesRepository(conn)
|
|
21
|
-
self.
|
|
21
|
+
self.vacancy_contacts = VacancyContactsRepository(conn)
|
|
22
22
|
self.negotiations = NegotiationRepository(conn)
|
|
23
23
|
self.settings = SettingsRepository(conn)
|
|
24
24
|
self.resumes = ResumesRepository(conn)
|
|
@@ -4,8 +4,8 @@ from datetime import datetime
|
|
|
4
4
|
from logging import getLogger
|
|
5
5
|
from typing import Any, Callable, Mapping, Self, dataclass_transform, get_origin
|
|
6
6
|
|
|
7
|
-
from hh_applicant_tool.utils import
|
|
8
|
-
from hh_applicant_tool.utils.
|
|
7
|
+
from hh_applicant_tool.utils import json
|
|
8
|
+
from hh_applicant_tool.utils.date import try_parse_datetime
|
|
9
9
|
|
|
10
10
|
logger = getLogger(__package__)
|
|
11
11
|
|
|
@@ -13,12 +13,15 @@ MISSING = object()
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def mapped(
|
|
16
|
+
*,
|
|
17
|
+
skip_src: bool = False,
|
|
16
18
|
path: str | None = None,
|
|
17
19
|
transform: Callable[[Any], Any] | None = None,
|
|
18
20
|
store_json: bool = False,
|
|
19
21
|
**kwargs: Any,
|
|
20
22
|
):
|
|
21
23
|
metadata = kwargs.get("metadata", {})
|
|
24
|
+
metadata.setdefault("skip_src", skip_src)
|
|
22
25
|
metadata.setdefault("path", path)
|
|
23
26
|
metadata.setdefault("transform", transform)
|
|
24
27
|
metadata.setdefault("store_json", store_json)
|
|
@@ -48,7 +51,7 @@ class BaseModel:
|
|
|
48
51
|
if value is MISSING:
|
|
49
52
|
continue
|
|
50
53
|
if f.metadata.get("store_json"):
|
|
51
|
-
value =
|
|
54
|
+
value = json.dumps(value)
|
|
52
55
|
# Точно не нужно типы приводить перед сохранением
|
|
53
56
|
# else:
|
|
54
57
|
# value = self._coerce_type(value, f)
|
|
@@ -89,6 +92,8 @@ class BaseModel:
|
|
|
89
92
|
kwargs = {}
|
|
90
93
|
for f in fields(cls):
|
|
91
94
|
if from_source:
|
|
95
|
+
if f.metadata.get("skip_src") and f.name in data:
|
|
96
|
+
continue
|
|
92
97
|
if path := f.metadata.get("path"):
|
|
93
98
|
found = True
|
|
94
99
|
v = data
|
|
@@ -118,7 +123,7 @@ class BaseModel:
|
|
|
118
123
|
continue
|
|
119
124
|
|
|
120
125
|
if f.metadata.get("store_json"):
|
|
121
|
-
value =
|
|
126
|
+
value = json.loads(value)
|
|
122
127
|
else:
|
|
123
128
|
value = cls._coerce_type(value, f)
|
|
124
129
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
|
|
3
|
+
from .base import BaseModel, mapped
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Из вакансии извлекается
|
|
7
|
+
class VacancyContactsModel(BaseModel):
|
|
8
|
+
# При вызове from_api на вакансии нужно игнорировать ее id
|
|
9
|
+
id: str = mapped(
|
|
10
|
+
skip_src=True,
|
|
11
|
+
default_factory=lambda: secrets.token_hex(16),
|
|
12
|
+
)
|
|
13
|
+
vacancy_id: int = mapped(path="id")
|
|
14
|
+
|
|
15
|
+
vacancy_name: str = mapped(path="name")
|
|
16
|
+
vacancy_alternate_url: str = mapped(path="alternate_url", default=None)
|
|
17
|
+
vacancy_area_id: int = mapped(path="area.id", default=None)
|
|
18
|
+
vacancy_area_name: str = mapped(path="area.name", default=None)
|
|
19
|
+
vacancy_salary_from: int = mapped(path="salary.from", default=0)
|
|
20
|
+
vacancy_salary_to: int = mapped(path="salary.to", default=0)
|
|
21
|
+
vacancy_currency: str = mapped(path="salary.currency", default="RUR")
|
|
22
|
+
vacancy_gross: bool = mapped(path="salary.gross", default=False)
|
|
23
|
+
|
|
24
|
+
employer_id: int = mapped(path="employer.id", default=None)
|
|
25
|
+
employer_name: str = mapped(path="employer.name", default=None)
|
|
26
|
+
email: str = mapped(path="contacts.email")
|
|
27
|
+
name: str = mapped(path="contacts.name", default=None)
|
|
28
|
+
phone_numbers: str = mapped(
|
|
29
|
+
path="contacts.phones",
|
|
30
|
+
transform=lambda phones: ", ".join(
|
|
31
|
+
p["formatted"] for p in phones if p.get("number")
|
|
32
|
+
),
|
|
33
|
+
default=None,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
self.vacancy_salary_from = (
|
|
38
|
+
self.vacancy_salary_from or self.vacancy_salary_to or 0
|
|
39
|
+
)
|
|
40
|
+
self.vacancy_salary_to = (
|
|
41
|
+
self.vacancy_salary_to or self.vacancy_salary_from or 0
|
|
42
|
+
)
|
|
@@ -14,17 +14,29 @@ CREATE TABLE IF NOT EXISTS employers (
|
|
|
14
14
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
15
15
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
16
16
|
);
|
|
17
|
-
/* =====================
|
|
18
|
-
CREATE TABLE IF NOT EXISTS
|
|
19
|
-
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
20
|
-
|
|
21
|
-
--
|
|
17
|
+
/* ===================== contacts ===================== */
|
|
18
|
+
CREATE TABLE IF NOT EXISTS vacancy_contacts (
|
|
19
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))) NOT NULL,
|
|
20
|
+
vacancy_id INTEGER NOT NULL,
|
|
21
|
+
-- Все это избыточные поля
|
|
22
|
+
vacancy_alternate_url TEXT,
|
|
23
|
+
vacancy_name TEXT,
|
|
24
|
+
vacancy_area_id INTEGER,
|
|
25
|
+
vacancy_area_name TEXT,
|
|
26
|
+
vacancy_salary_from INTEGER,
|
|
27
|
+
vacancy_salary_to INTEGER,
|
|
28
|
+
vacancy_currency VARCHAR(3),
|
|
29
|
+
vacancy_gross BOOLEAN,
|
|
30
|
+
--
|
|
31
|
+
employer_id INTEGER,
|
|
32
|
+
employer_name TEXT,
|
|
33
|
+
--
|
|
22
34
|
name TEXT,
|
|
23
35
|
email TEXT,
|
|
24
36
|
phone_numbers TEXT NOT NULL,
|
|
25
37
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
26
38
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
-
UNIQUE (
|
|
39
|
+
UNIQUE (vacancy_id, email)
|
|
28
40
|
);
|
|
29
41
|
/* ===================== vacancies ===================== */
|
|
30
42
|
CREATE TABLE IF NOT EXISTS vacancies (
|
|
@@ -49,7 +61,8 @@ CREATE TABLE IF NOT EXISTS negotiations (
|
|
|
49
61
|
id INTEGER PRIMARY KEY,
|
|
50
62
|
state TEXT NOT NULL,
|
|
51
63
|
vacancy_id INTEGER NOT NULL,
|
|
52
|
-
employer_id INTEGER
|
|
64
|
+
employer_id INTEGER,
|
|
65
|
+
-- Может обнулиться при блокировке раб-о-тодателя
|
|
53
66
|
chat_id INTEGER NOT NULL,
|
|
54
67
|
resume_id TEXT,
|
|
55
68
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -95,10 +108,10 @@ UPDATE employers
|
|
|
95
108
|
SET updated_at = CURRENT_TIMESTAMP
|
|
96
109
|
WHERE id = OLD.id;
|
|
97
110
|
END;
|
|
98
|
-
CREATE TRIGGER IF NOT EXISTS
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS trg_vacancy_contacts_updated
|
|
99
112
|
AFTER
|
|
100
|
-
UPDATE ON
|
|
101
|
-
UPDATE
|
|
113
|
+
UPDATE ON vacancy_contacts BEGIN
|
|
114
|
+
UPDATE vacancy_contacts
|
|
102
115
|
SET updated_at = CURRENT_TIMESTAMP
|
|
103
116
|
WHERE id = OLD.id;
|
|
104
117
|
END;
|