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,12 +1,18 @@
|
|
|
1
1
|
# Этот модуль можно использовать как образец для других
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
2
4
|
import argparse
|
|
3
5
|
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..api import ApiError, datatypes
|
|
9
|
+
from ..main import BaseNamespace, BaseOperation
|
|
10
|
+
from ..utils import print_err
|
|
11
|
+
from ..utils.string import shorten
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..main import HHApplicantTool
|
|
4
15
|
|
|
5
|
-
from ..api import ApiClient, ApiError
|
|
6
|
-
from ..main import BaseOperation
|
|
7
|
-
from ..main import Namespace as BaseNamespace
|
|
8
|
-
from ..types import ApiListResponse
|
|
9
|
-
from ..utils import print_err, truncate_string
|
|
10
16
|
|
|
11
17
|
logger = logging.getLogger(__package__)
|
|
12
18
|
|
|
@@ -18,15 +24,21 @@ class Namespace(BaseNamespace):
|
|
|
18
24
|
class Operation(BaseOperation):
|
|
19
25
|
"""Обновить все резюме"""
|
|
20
26
|
|
|
27
|
+
__aliases__ = ["update"]
|
|
28
|
+
|
|
21
29
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
22
30
|
pass
|
|
23
31
|
|
|
24
|
-
def run(self,
|
|
25
|
-
resumes:
|
|
26
|
-
|
|
32
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
33
|
+
resumes: list[datatypes.Resume] = tool.get_resumes()
|
|
34
|
+
# Там вызов API меняет поля
|
|
35
|
+
# tool.storage.resumes.save_batch(resumes)
|
|
36
|
+
for resume in resumes:
|
|
27
37
|
try:
|
|
28
|
-
res = api_client.post(
|
|
38
|
+
res = tool.api_client.post(
|
|
39
|
+
f"/resumes/{resume['id']}/publish",
|
|
40
|
+
)
|
|
29
41
|
assert res == {}
|
|
30
|
-
print("✅ Обновлено",
|
|
42
|
+
print("✅ Обновлено", shorten(resume["title"]))
|
|
31
43
|
except ApiError as ex:
|
|
32
|
-
print_err("❗
|
|
44
|
+
print_err("❗", ex)
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Этот модуль можно использовать как образец для других
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
2
4
|
import argparse
|
|
3
|
-
import json
|
|
4
5
|
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..api import datatypes
|
|
9
|
+
from ..main import BaseNamespace, BaseOperation
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..main import HHApplicantTool
|
|
5
13
|
|
|
6
|
-
from ..api import ApiClient
|
|
7
|
-
from ..main import BaseOperation
|
|
8
|
-
from ..main import Namespace as BaseNamespace
|
|
9
14
|
|
|
10
15
|
logger = logging.getLogger(__package__)
|
|
11
16
|
|
|
@@ -14,12 +19,40 @@ class Namespace(BaseNamespace):
|
|
|
14
19
|
pass
|
|
15
20
|
|
|
16
21
|
|
|
22
|
+
def fmt_plus(n: int) -> str:
|
|
23
|
+
assert n >= 0
|
|
24
|
+
return f"+{n}" if n else "0"
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
class Operation(BaseOperation):
|
|
18
28
|
"""Выведет текущего пользователя"""
|
|
19
29
|
|
|
30
|
+
__aliases__: list[str] = ["id"]
|
|
31
|
+
|
|
20
32
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
21
33
|
pass
|
|
22
34
|
|
|
23
|
-
def run(self,
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
def run(self, tool: HHApplicantTool) -> None:
|
|
36
|
+
api_client = tool.api_client
|
|
37
|
+
result: datatypes.User = api_client.get("me")
|
|
38
|
+
full_name = " ".join(
|
|
39
|
+
filter(
|
|
40
|
+
None,
|
|
41
|
+
[
|
|
42
|
+
result.get("last_name"),
|
|
43
|
+
result.get("first_name"),
|
|
44
|
+
result.get("middle_name"),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
with tool.storage.settings as s:
|
|
49
|
+
s.set_value("user.full_name", full_name)
|
|
50
|
+
s.set_value("user.email", result.get("email"))
|
|
51
|
+
s.set_value("user.phone", result.get("phone"))
|
|
52
|
+
counters = result["counters"]
|
|
53
|
+
print(
|
|
54
|
+
f"🆔 {result['id']} {full_name or 'Анонимный аккаунт'} "
|
|
55
|
+
f"[ 📄 {counters['resumes_count']} "
|
|
56
|
+
f"| 👁️ {fmt_plus(counters['new_resume_views'])} "
|
|
57
|
+
f"| ✉️ {fmt_plus(counters['unread_negotiations'])} ]"
|
|
58
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
|
|
5
|
+
from .repositories.contacts import VacancyContactsRepository
|
|
6
|
+
from .repositories.employers import EmployersRepository
|
|
7
|
+
from .repositories.negotiations import NegotiationRepository
|
|
8
|
+
from .repositories.resumes import ResumesRepository
|
|
9
|
+
from .repositories.settings import SettingsRepository
|
|
10
|
+
from .repositories.vacancies import VacanciesRepository
|
|
11
|
+
from .utils import init_db
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StorageFacade:
|
|
15
|
+
"""Единая точка доступа к persistence-слою."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
18
|
+
init_db(conn)
|
|
19
|
+
self.employers = EmployersRepository(conn)
|
|
20
|
+
self.vacancies = VacanciesRepository(conn)
|
|
21
|
+
self.vacancy_contacts = VacancyContactsRepository(conn)
|
|
22
|
+
self.negotiations = NegotiationRepository(conn)
|
|
23
|
+
self.settings = SettingsRepository(conn)
|
|
24
|
+
self.resumes = ResumesRepository(conn)
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
from dataclasses import Field, asdict, dataclass, field, fields
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from typing import Any, Callable, Mapping, Self, dataclass_transform, get_origin
|
|
6
|
+
|
|
7
|
+
from hh_applicant_tool.utils import json
|
|
8
|
+
from hh_applicant_tool.utils.date import try_parse_datetime
|
|
9
|
+
|
|
10
|
+
logger = getLogger(__package__)
|
|
11
|
+
|
|
12
|
+
MISSING = object()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mapped(
|
|
16
|
+
path: str | None = None,
|
|
17
|
+
transform: Callable[[Any], Any] | None = None,
|
|
18
|
+
store_json: bool = False,
|
|
19
|
+
**kwargs: Any,
|
|
20
|
+
):
|
|
21
|
+
metadata = kwargs.get("metadata", {})
|
|
22
|
+
metadata.setdefault("path", path)
|
|
23
|
+
metadata.setdefault("transform", transform)
|
|
24
|
+
metadata.setdefault("store_json", store_json)
|
|
25
|
+
return field(metadata=metadata, **kwargs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass_transform(field_specifiers=(field, mapped))
|
|
29
|
+
class BaseModel:
|
|
30
|
+
def __init_subclass__(cls, /, **kwargs: Any):
|
|
31
|
+
super().__init_subclass__()
|
|
32
|
+
dataclass(cls, kw_only=True, **kwargs)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_db(cls, data: Mapping[str, Any]) -> Self:
|
|
36
|
+
return cls._from_mapping(data)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_api(cls, data: Mapping[str, Any]) -> Self:
|
|
40
|
+
return cls._from_mapping(data, from_source=True)
|
|
41
|
+
|
|
42
|
+
def to_db(self) -> dict[str, Any]:
|
|
43
|
+
data = self.to_dict()
|
|
44
|
+
for f in fields(self):
|
|
45
|
+
# Если какого-то значения нет в словаре, то не ставим его или
|
|
46
|
+
# ломается установка дефолтных значений.
|
|
47
|
+
value = data.get(f.name, MISSING)
|
|
48
|
+
if value is MISSING:
|
|
49
|
+
continue
|
|
50
|
+
if f.metadata.get("store_json"):
|
|
51
|
+
value = json.dumps(value)
|
|
52
|
+
# Точно не нужно типы приводить перед сохранением
|
|
53
|
+
# else:
|
|
54
|
+
# value = self._coerce_type(value, f)
|
|
55
|
+
data[f.name] = value
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def _coerce_type(cls, value: Any, f: Field) -> Any:
|
|
60
|
+
# Лишь создатель знает, что с тобой делать
|
|
61
|
+
if get_origin(f.type):
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
type_name = f.type if isinstance(f.type, str) else f.type.__name__
|
|
65
|
+
if value is not None and type_name in (
|
|
66
|
+
"bool",
|
|
67
|
+
"str",
|
|
68
|
+
"int",
|
|
69
|
+
"float",
|
|
70
|
+
"datetime",
|
|
71
|
+
):
|
|
72
|
+
if type_name == "datetime":
|
|
73
|
+
return try_parse_datetime(value)
|
|
74
|
+
try:
|
|
75
|
+
t = getattr(builtins, type_name)
|
|
76
|
+
if not isinstance(value, t):
|
|
77
|
+
value = t(value)
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
pass
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def _from_mapping(
|
|
84
|
+
cls,
|
|
85
|
+
data: Mapping[str, Any],
|
|
86
|
+
/,
|
|
87
|
+
from_source: bool = False,
|
|
88
|
+
) -> Self:
|
|
89
|
+
kwargs = {}
|
|
90
|
+
for f in fields(cls):
|
|
91
|
+
if from_source:
|
|
92
|
+
if path := f.metadata.get("path"):
|
|
93
|
+
found = True
|
|
94
|
+
v = data
|
|
95
|
+
for key in path.split("."):
|
|
96
|
+
if isinstance(v, Mapping):
|
|
97
|
+
v = v.get(key)
|
|
98
|
+
else:
|
|
99
|
+
found = False
|
|
100
|
+
break
|
|
101
|
+
if not found:
|
|
102
|
+
continue
|
|
103
|
+
value = v
|
|
104
|
+
else:
|
|
105
|
+
value = data.get(f.name, MISSING)
|
|
106
|
+
if value is MISSING:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if value is not None and (t := f.metadata.get("transform")):
|
|
110
|
+
if isinstance(t, str):
|
|
111
|
+
t = getattr(cls, t)
|
|
112
|
+
value = t(value)
|
|
113
|
+
|
|
114
|
+
value = cls._coerce_type(value, f)
|
|
115
|
+
else:
|
|
116
|
+
value = data.get(f.name, MISSING)
|
|
117
|
+
if value is MISSING:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if f.metadata.get("store_json"):
|
|
121
|
+
value = json.loads(value)
|
|
122
|
+
else:
|
|
123
|
+
value = cls._coerce_type(value, f)
|
|
124
|
+
|
|
125
|
+
kwargs[f.name] = value
|
|
126
|
+
return cls(**kwargs)
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> dict[str, Any]:
|
|
129
|
+
return asdict(self) # pyright: ignore[reportArgumentType]
|
|
130
|
+
|
|
131
|
+
# def to_json(self, **kwargs: Any) -> str:
|
|
132
|
+
# """Serializes the model to a JSON string."""
|
|
133
|
+
# kwargs.setdefault("ensure_ascii", False)
|
|
134
|
+
# return json_utils.dumps(self.to_dict(), **kwargs)
|
|
135
|
+
|
|
136
|
+
# @classmethod
|
|
137
|
+
# def from_json(cls, json_str: str, **kwargs: Any) -> Self:
|
|
138
|
+
# """Deserializes a model from a JSON string."""
|
|
139
|
+
# data = json_utils.loads(json_str, **kwargs)
|
|
140
|
+
# # from_api is probably more appropriate as JSON is a common API format
|
|
141
|
+
# # and it handles nested data sources.
|
|
142
|
+
# return cls.from_api(data)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
|
|
147
|
+
class CompanyModel(BaseModel):
|
|
148
|
+
id: "int"
|
|
149
|
+
name: str
|
|
150
|
+
city_id: int = mapped(path="location.city.id")
|
|
151
|
+
city: str = mapped(path="location.city.name")
|
|
152
|
+
created_at: datetime
|
|
153
|
+
|
|
154
|
+
c = CompanyModel.from_api(
|
|
155
|
+
{
|
|
156
|
+
"id": "42",
|
|
157
|
+
"name": "ACME",
|
|
158
|
+
"location": {
|
|
159
|
+
"city": {
|
|
160
|
+
"id": "1",
|
|
161
|
+
"name": "Moscow",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
"created_at": "2026-01-09T04:12:00.114858",
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
print(c)
|
|
169
|
+
# assert c == CompanyModel(id=42, name="ACME", city_id=1, city="Moscow")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .base import BaseModel, mapped
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Из вакансии извлекается
|
|
5
|
+
class VacancyContactsModel(BaseModel):
|
|
6
|
+
id: int
|
|
7
|
+
vacancy_id: int = mapped(path="id")
|
|
8
|
+
|
|
9
|
+
vacancy_name: str = mapped(path="name")
|
|
10
|
+
vacancy_alternate_url: str = mapped(path="alternate_url", default=None)
|
|
11
|
+
vacancy_area_id: int = mapped(path="area.id", default=None)
|
|
12
|
+
vacancy_area_name: str = mapped(path="area.name", default=None)
|
|
13
|
+
vacancy_salary_from: int = mapped(path="salary.from", default=0)
|
|
14
|
+
vacancy_salary_to: int = mapped(path="salary.to", default=0)
|
|
15
|
+
vacancy_currency: str = mapped(path="salary.currency", default="RUR")
|
|
16
|
+
vacancy_gross: bool = mapped(path="salary.gross", default=False)
|
|
17
|
+
|
|
18
|
+
employer_id: int = mapped(path="employer.id", default=None)
|
|
19
|
+
employer_name: str = mapped(path="employer.name", default=None)
|
|
20
|
+
email: str = mapped(path="contacts.email")
|
|
21
|
+
name: str = mapped(path="contacts.name", default=None)
|
|
22
|
+
phone_numbers: str = mapped(
|
|
23
|
+
path="contacts.phones",
|
|
24
|
+
transform=lambda phones: ", ".join(
|
|
25
|
+
p["formatted"] for p in phones if p.get("number")
|
|
26
|
+
),
|
|
27
|
+
default=None,
|
|
28
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .base import BaseModel, mapped
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EmployerModel(BaseModel):
|
|
5
|
+
id: int
|
|
6
|
+
name: str
|
|
7
|
+
type: str | None = None
|
|
8
|
+
description: str | None = None
|
|
9
|
+
site_url: str | None = None
|
|
10
|
+
alternate_url: str | None = None
|
|
11
|
+
area_id: int = mapped(path="area.id", default=None)
|
|
12
|
+
area_name: str = mapped(path="area.name", default=None)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from .base import BaseModel, mapped
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NegotiationModel(BaseModel):
|
|
9
|
+
id: int
|
|
10
|
+
chat_id: int
|
|
11
|
+
state: str = mapped(path="state.id")
|
|
12
|
+
vacancy_id: int = mapped(path="vacancy.id")
|
|
13
|
+
employer_id: int = mapped(path="vacancy.employer.id", default=None)
|
|
14
|
+
resume_id: str = mapped(path="resume.id")
|
|
15
|
+
created_at: datetime | None = None
|
|
16
|
+
updated_at: datetime | None = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from .base import BaseModel, mapped
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResumeModel(BaseModel):
|
|
9
|
+
id: str
|
|
10
|
+
title: str
|
|
11
|
+
url: str
|
|
12
|
+
alternate_url: str
|
|
13
|
+
status_id: str = mapped(path="status.id")
|
|
14
|
+
status_name: str = mapped(path="status.name")
|
|
15
|
+
can_publish_or_update: bool = False
|
|
16
|
+
total_views: int = mapped(path="counters.total_views", default=0)
|
|
17
|
+
new_views: int = mapped(path="counters.new_views", default=0)
|
|
18
|
+
created_at: datetime | None = None
|
|
19
|
+
updated_at: datetime | None = None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from .base import BaseModel, mapped
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VacancyModel(BaseModel):
|
|
9
|
+
id: int
|
|
10
|
+
name: str
|
|
11
|
+
alternate_url: str
|
|
12
|
+
area_id: int = mapped(path="area.id")
|
|
13
|
+
area_name: str = mapped(path="area.name")
|
|
14
|
+
salary_from: int = mapped(path="salary.from", default=None)
|
|
15
|
+
salary_to: int = mapped(path="salary.to", default=None)
|
|
16
|
+
currency: str = mapped(path="salary.currency", default="RUR")
|
|
17
|
+
gross: bool = mapped(path="salary.gross", default=False)
|
|
18
|
+
|
|
19
|
+
remote: bool = mapped(
|
|
20
|
+
path="schedule.id",
|
|
21
|
+
transform=lambda v: v == "remote",
|
|
22
|
+
default=False,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
experience: str = mapped(path="experience.id", default=None)
|
|
26
|
+
professional_roles: list[dict] = mapped(
|
|
27
|
+
store_json=True, default_factory=list
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
created_at: datetime | None = None
|
|
31
|
+
published_at: datetime | None = None
|
|
32
|
+
updated_at: datetime | None = None
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
self.salary_from = self.salary_from or self.salary_to or 0
|
|
36
|
+
self.salary_to = self.salary_to or self.salary_from or 0
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
PRAGMA foreign_keys = OFF;
|
|
2
|
+
-- На всякий случай выключаем проверки
|
|
3
|
+
BEGIN;
|
|
4
|
+
/* ===================== employers ===================== */
|
|
5
|
+
CREATE TABLE IF NOT EXISTS employers (
|
|
6
|
+
id INTEGER PRIMARY KEY,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
type TEXT,
|
|
9
|
+
description TEXT,
|
|
10
|
+
site_url TEXT,
|
|
11
|
+
area_id INTEGER,
|
|
12
|
+
area_name TEXT,
|
|
13
|
+
alternate_url TEXT,
|
|
14
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
15
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
);
|
|
17
|
+
/* ===================== contacts ===================== */
|
|
18
|
+
CREATE TABLE IF NOT EXISTS vacancy_contacts (
|
|
19
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
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
|
+
--
|
|
34
|
+
name TEXT,
|
|
35
|
+
email TEXT,
|
|
36
|
+
phone_numbers TEXT NOT NULL,
|
|
37
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
38
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
39
|
+
UNIQUE (vacancy_id, email)
|
|
40
|
+
);
|
|
41
|
+
/* ===================== vacancies ===================== */
|
|
42
|
+
CREATE TABLE IF NOT EXISTS vacancies (
|
|
43
|
+
id INTEGER PRIMARY KEY,
|
|
44
|
+
name TEXT NOT NULL,
|
|
45
|
+
area_id INTEGER,
|
|
46
|
+
area_name TEXT,
|
|
47
|
+
salary_from INTEGER,
|
|
48
|
+
salary_to INTEGER,
|
|
49
|
+
currency VARCHAR(3),
|
|
50
|
+
gross BOOLEAN,
|
|
51
|
+
published_at DATETIME,
|
|
52
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
53
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
54
|
+
remote BOOLEAN,
|
|
55
|
+
experience TEXT,
|
|
56
|
+
professional_roles TEXT,
|
|
57
|
+
alternate_url TEXT
|
|
58
|
+
);
|
|
59
|
+
/* ===================== negotiations ===================== */
|
|
60
|
+
CREATE TABLE IF NOT EXISTS negotiations (
|
|
61
|
+
id INTEGER PRIMARY KEY,
|
|
62
|
+
state TEXT NOT NULL,
|
|
63
|
+
vacancy_id INTEGER NOT NULL,
|
|
64
|
+
employer_id INTEGER,
|
|
65
|
+
-- Может обнулиться при блокировке раб-о-тодателя
|
|
66
|
+
chat_id INTEGER NOT NULL,
|
|
67
|
+
resume_id TEXT,
|
|
68
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
);
|
|
71
|
+
/* ===================== settings ===================== */
|
|
72
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
73
|
+
key TEXT PRIMARY KEY,
|
|
74
|
+
value TEXT NOT NULL
|
|
75
|
+
);
|
|
76
|
+
/* ===================== resumes ===================== */
|
|
77
|
+
CREATE TABLE IF NOT EXISTS resumes (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
title TEXT NOT NULL,
|
|
80
|
+
url TEXT,
|
|
81
|
+
alternate_url TEXT,
|
|
82
|
+
status_id TEXT,
|
|
83
|
+
status_name TEXT,
|
|
84
|
+
can_publish_or_update BOOLEAN,
|
|
85
|
+
total_views INTEGER DEFAULT 0,
|
|
86
|
+
new_views INTEGER DEFAULT 0,
|
|
87
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
88
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
89
|
+
);
|
|
90
|
+
/* ===================== ИНДЕКСЫ ДЛЯ СТАТИСТИКИ ===================== */
|
|
91
|
+
-- Чтобы выборка для отправки на сервер по updated_at не тормозила
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_vac_upd ON vacancies(updated_at);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_emp_upd ON employers(updated_at);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_neg_upd ON negotiations(updated_at);
|
|
95
|
+
/* ===================== ТРИГГЕРЫ (Всегда обновляют дату) ===================== */
|
|
96
|
+
-- Убрал условие WHEN. Теперь при любом UPDATE дата актуализируется принудительно.
|
|
97
|
+
CREATE TRIGGER IF NOT EXISTS trg_resumes_updated
|
|
98
|
+
AFTER
|
|
99
|
+
UPDATE ON resumes BEGIN
|
|
100
|
+
UPDATE resumes
|
|
101
|
+
SET updated_at = CURRENT_TIMESTAMP
|
|
102
|
+
WHERE id = OLD.id;
|
|
103
|
+
END;
|
|
104
|
+
CREATE TRIGGER IF NOT EXISTS trg_employers_updated
|
|
105
|
+
AFTER
|
|
106
|
+
UPDATE ON employers BEGIN
|
|
107
|
+
UPDATE employers
|
|
108
|
+
SET updated_at = CURRENT_TIMESTAMP
|
|
109
|
+
WHERE id = OLD.id;
|
|
110
|
+
END;
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS trg_vacancy_contacts_updated
|
|
112
|
+
AFTER
|
|
113
|
+
UPDATE ON vacancy_contacts BEGIN
|
|
114
|
+
UPDATE vacancy_contacts
|
|
115
|
+
SET updated_at = CURRENT_TIMESTAMP
|
|
116
|
+
WHERE id = OLD.id;
|
|
117
|
+
END;
|
|
118
|
+
CREATE TRIGGER IF NOT EXISTS trg_vacancies_updated
|
|
119
|
+
AFTER
|
|
120
|
+
UPDATE ON vacancies BEGIN
|
|
121
|
+
UPDATE vacancies
|
|
122
|
+
SET updated_at = CURRENT_TIMESTAMP
|
|
123
|
+
WHERE id = OLD.id;
|
|
124
|
+
END;
|
|
125
|
+
CREATE TRIGGER IF NOT EXISTS trg_negotiations_updated
|
|
126
|
+
AFTER
|
|
127
|
+
UPDATE ON negotiations BEGIN
|
|
128
|
+
UPDATE negotiations
|
|
129
|
+
SET updated_at = CURRENT_TIMESTAMP
|
|
130
|
+
WHERE id = OLD.id;
|
|
131
|
+
END;
|
|
132
|
+
COMMIT;
|
|
File without changes
|