jobty 0.1.0__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.
- jobty-0.1.0/PKG-INFO +96 -0
- jobty-0.1.0/README.md +84 -0
- jobty-0.1.0/pyproject.toml +21 -0
- jobty-0.1.0/src/jobty/__init__.py +0 -0
- jobty-0.1.0/src/jobty/constants.py +14 -0
- jobty-0.1.0/src/jobty/database.py +26 -0
- jobty-0.1.0/src/jobty/main.py +186 -0
- jobty-0.1.0/src/jobty/models.py +35 -0
- jobty-0.1.0/src/jobty/print_helpers.py +97 -0
- jobty-0.1.0/src/jobty/schemas.py +62 -0
- jobty-0.1.0/src/jobty/services.py +80 -0
- jobty-0.1.0/src/jobty/validations.py +15 -0
jobty-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: jobty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: El John
|
|
6
|
+
Author-email: El John <bongaeljohn07@gmail.com>
|
|
7
|
+
Requires-Dist: pydantic>=2.12.5
|
|
8
|
+
Requires-Dist: sqlalchemy>=2.0.49
|
|
9
|
+
Requires-Dist: typer>=0.24.1
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Jobty
|
|
14
|
+
|
|
15
|
+
A simple command-line application to track your job applications.
|
|
16
|
+
|
|
17
|
+
This project helps you manage every job application in one place with commands for listing, adding, updating, and deleting job application entries. I originally created this project for myself to keep track of my job applications.
|
|
18
|
+
|
|
19
|
+
## What this project is
|
|
20
|
+
|
|
21
|
+
`jobty` is a Python CLI app built to track job applications you submit.
|
|
22
|
+
It stores application details locally and provides a quick interface to:
|
|
23
|
+
|
|
24
|
+
- add new job application entry
|
|
25
|
+
- view all saved application entries
|
|
26
|
+
- inspect a single job application entry
|
|
27
|
+
- update a single job application entry
|
|
28
|
+
- delete a single job application entry
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.13 or newer
|
|
33
|
+
- `pydantic`
|
|
34
|
+
- `questionary`
|
|
35
|
+
- `typer`
|
|
36
|
+
|
|
37
|
+
## How to clone and run it
|
|
38
|
+
|
|
39
|
+
> [!IMPORTANT]
|
|
40
|
+
> You must already have [uv](https://docs.astral.sh/uv/) installed on your machine. If not, you can check out the documentation [here](https://docs.astral.sh/uv/getting-started/installation/).
|
|
41
|
+
|
|
42
|
+
1. Clone the repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/eljohn316/jobty.git
|
|
46
|
+
cd jobty
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. Create and activate a virtual environment with uv:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv venv .venv
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
3. Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Example commands
|
|
63
|
+
|
|
64
|
+
- List all job applications entries:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
jobty list
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- View a single application by ID:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
jobty list-one <job_id>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Add a new application:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jobty add
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- Update an existing application:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
jobty update <job_id>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- Delete an application:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
jobty delete <job_id>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Notes
|
|
95
|
+
|
|
96
|
+
When run, the CLI initializes the local job applications storage automatically, so you can start adding applications right away.
|
jobty-0.1.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Jobty
|
|
2
|
+
|
|
3
|
+
A simple command-line application to track your job applications.
|
|
4
|
+
|
|
5
|
+
This project helps you manage every job application in one place with commands for listing, adding, updating, and deleting job application entries. I originally created this project for myself to keep track of my job applications.
|
|
6
|
+
|
|
7
|
+
## What this project is
|
|
8
|
+
|
|
9
|
+
`jobty` is a Python CLI app built to track job applications you submit.
|
|
10
|
+
It stores application details locally and provides a quick interface to:
|
|
11
|
+
|
|
12
|
+
- add new job application entry
|
|
13
|
+
- view all saved application entries
|
|
14
|
+
- inspect a single job application entry
|
|
15
|
+
- update a single job application entry
|
|
16
|
+
- delete a single job application entry
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.13 or newer
|
|
21
|
+
- `pydantic`
|
|
22
|
+
- `questionary`
|
|
23
|
+
- `typer`
|
|
24
|
+
|
|
25
|
+
## How to clone and run it
|
|
26
|
+
|
|
27
|
+
> [!IMPORTANT]
|
|
28
|
+
> You must already have [uv](https://docs.astral.sh/uv/) installed on your machine. If not, you can check out the documentation [here](https://docs.astral.sh/uv/getting-started/installation/).
|
|
29
|
+
|
|
30
|
+
1. Clone the repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/eljohn316/jobty.git
|
|
34
|
+
cd jobty
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. Create and activate a virtual environment with uv:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv venv .venv
|
|
41
|
+
source .venv/bin/activate
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
3. Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv sync
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Example commands
|
|
51
|
+
|
|
52
|
+
- List all job applications entries:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
jobty list
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- View a single application by ID:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
jobty list-one <job_id>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Add a new application:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
jobty add
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- Update an existing application:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
jobty update <job_id>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Delete an application:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
jobty delete <job_id>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
When run, the CLI initializes the local job applications storage automatically, so you can start adding applications right away.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jobty"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "El John", email = "bongaeljohn07@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pydantic>=2.12.5",
|
|
12
|
+
"sqlalchemy>=2.0.49",
|
|
13
|
+
"typer>=0.24.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
jobty = "jobty.main:app"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.9.15,<0.10.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Colors(Enum):
|
|
6
|
+
red = "#ef4444"
|
|
7
|
+
emerald = "#34d399"
|
|
8
|
+
amber = "#fbbf24"
|
|
9
|
+
gray = "#9ca3af"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Arrangement = Literal["Onsite", "Hybrid", "Remote"]
|
|
13
|
+
|
|
14
|
+
Status = Literal["Applied", "Interview", "Hired", "Rejected"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import create_engine
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|
5
|
+
|
|
6
|
+
SQLALCHEMY_DATABASE_URL = "sqlite:///jobs.db"
|
|
7
|
+
|
|
8
|
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
|
9
|
+
|
|
10
|
+
Session = sessionmaker(bind=engine, autoflush=False)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Base(DeclarativeBase):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def get_db_session():
|
|
19
|
+
db = Session()
|
|
20
|
+
try:
|
|
21
|
+
yield db
|
|
22
|
+
except Exception:
|
|
23
|
+
db.rollback()
|
|
24
|
+
raise
|
|
25
|
+
finally:
|
|
26
|
+
db.close()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typer import Argument, Option
|
|
7
|
+
|
|
8
|
+
from .constants import Arrangement, Status
|
|
9
|
+
from .database import Base, engine
|
|
10
|
+
from .print_helpers import print_job, print_jobs
|
|
11
|
+
from .schemas import JobCreate, JobDetails, JobUpdate
|
|
12
|
+
from .services import create_job, delete_job, get_job, get_jobs, update_job
|
|
13
|
+
from .validations import validate_model
|
|
14
|
+
|
|
15
|
+
Base.metadata.create_all(bind=engine)
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FilterStatus(str, Enum):
|
|
21
|
+
applied = "Applied"
|
|
22
|
+
interview = "Interview"
|
|
23
|
+
hired = "Hired"
|
|
24
|
+
rejected = "Rejected"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkArrangement(str, Enum):
|
|
28
|
+
onsite = "Onsite"
|
|
29
|
+
hybrid = "Hybrid"
|
|
30
|
+
remote = "Remote"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
def list(
|
|
35
|
+
job_id: Annotated[
|
|
36
|
+
int,
|
|
37
|
+
Argument(help="The ID of the Job to list", metavar="job_id"),
|
|
38
|
+
] = None,
|
|
39
|
+
status: Annotated[list[FilterStatus], Option(help="Filter by status")] = [],
|
|
40
|
+
work_arrangement: Annotated[
|
|
41
|
+
list[WorkArrangement], Option(help="Filter by work arrangement")
|
|
42
|
+
] = [],
|
|
43
|
+
):
|
|
44
|
+
"""List all job application entries or a single job application entry if job id is provided."""
|
|
45
|
+
if job_id:
|
|
46
|
+
job = get_job(job_id)
|
|
47
|
+
if job is None:
|
|
48
|
+
typer.echo("Job application not found")
|
|
49
|
+
return
|
|
50
|
+
job_details = JobDetails(**job.__dict__)
|
|
51
|
+
print_job(job_details)
|
|
52
|
+
else:
|
|
53
|
+
jobs = get_jobs(
|
|
54
|
+
status=[s.value for s in status],
|
|
55
|
+
work_arrangement=[w.value for w in work_arrangement],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if len(jobs) == 0:
|
|
59
|
+
typer.echo("You have no job applications yet.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
print_jobs(jobs)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def add(
|
|
67
|
+
role: Annotated[str, Option(help="Job role")],
|
|
68
|
+
company: Annotated[str, Option(help="Company name")],
|
|
69
|
+
location: Annotated[str, Option(help="Location")],
|
|
70
|
+
work_arrangement: Annotated[Arrangement, Option(help="Work arrangement")],
|
|
71
|
+
status: Annotated[Status, Option(help="Application status")] = "Applied",
|
|
72
|
+
source_link: Annotated[str | None, Option(help="Source link")] = None,
|
|
73
|
+
interview_date: Annotated[
|
|
74
|
+
datetime | None,
|
|
75
|
+
Option(
|
|
76
|
+
help="Interview date",
|
|
77
|
+
formats=["%m-%d-%Y"],
|
|
78
|
+
),
|
|
79
|
+
] = None,
|
|
80
|
+
interview_time: Annotated[
|
|
81
|
+
datetime | None,
|
|
82
|
+
Option(
|
|
83
|
+
help="Interview time",
|
|
84
|
+
formats=["%I:%M %p"],
|
|
85
|
+
),
|
|
86
|
+
] = None,
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Add job application entry.
|
|
90
|
+
"""
|
|
91
|
+
job_create_model = validate_model(
|
|
92
|
+
JobCreate,
|
|
93
|
+
{
|
|
94
|
+
"role": role,
|
|
95
|
+
"company": company,
|
|
96
|
+
"location": location,
|
|
97
|
+
"work_arrangement": work_arrangement,
|
|
98
|
+
"status": status,
|
|
99
|
+
"source_link": source_link,
|
|
100
|
+
"interview_date": interview_date.date() if interview_date else None,
|
|
101
|
+
"interview_time": interview_time.time() if interview_time else None,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
create_job(job_create_model)
|
|
105
|
+
typer.echo("Job application added")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def update(
|
|
110
|
+
job_id: Annotated[
|
|
111
|
+
int,
|
|
112
|
+
Argument(help="The ID of the Job to update", metavar="job_id"),
|
|
113
|
+
],
|
|
114
|
+
role: Annotated[str | None, Option(help="Job role")] = None,
|
|
115
|
+
company: Annotated[str | None, Option(help="Company name")] = None,
|
|
116
|
+
location: Annotated[str | None, Option(help="Location")] = None,
|
|
117
|
+
work_arrangement: Annotated[
|
|
118
|
+
Arrangement | None, Option(help="Work arrangement")
|
|
119
|
+
] = None,
|
|
120
|
+
status: Annotated[Status | None, Option(help="Application status")] = None,
|
|
121
|
+
source_link: Annotated[str | None, Option(help="Source link")] = None,
|
|
122
|
+
interview_date: Annotated[
|
|
123
|
+
datetime | None,
|
|
124
|
+
Option(
|
|
125
|
+
help="Interview date",
|
|
126
|
+
formats=["%m-%d-%Y"],
|
|
127
|
+
),
|
|
128
|
+
] = None,
|
|
129
|
+
interview_time: Annotated[
|
|
130
|
+
datetime | None,
|
|
131
|
+
Option(
|
|
132
|
+
help="Interview time",
|
|
133
|
+
formats=["%I:%M %p"],
|
|
134
|
+
),
|
|
135
|
+
] = None,
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Update an existing job application entry.
|
|
139
|
+
"""
|
|
140
|
+
if (
|
|
141
|
+
role is None
|
|
142
|
+
and company is None
|
|
143
|
+
and location is None
|
|
144
|
+
and work_arrangement is None
|
|
145
|
+
and status is None
|
|
146
|
+
and source_link is None
|
|
147
|
+
and interview_date is None
|
|
148
|
+
and interview_time is None
|
|
149
|
+
):
|
|
150
|
+
typer.echo("No fields to update")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
job_update = JobUpdate.model_validate(
|
|
154
|
+
{
|
|
155
|
+
"role": role,
|
|
156
|
+
"company": company,
|
|
157
|
+
"location": location,
|
|
158
|
+
"work_arrangement": work_arrangement,
|
|
159
|
+
"status": status,
|
|
160
|
+
"source_link": source_link,
|
|
161
|
+
"interview_date": interview_date.date() if interview_date else None,
|
|
162
|
+
"interview_time": interview_time.time() if interview_time else None,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
updated_job_id = update_job(job_id, job_update)
|
|
167
|
+
typer.echo(f"Job #{updated_job_id} updated")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.command()
|
|
171
|
+
def delete(
|
|
172
|
+
job_id: Annotated[
|
|
173
|
+
int,
|
|
174
|
+
Argument(help="The ID of the Job to delete", metavar="job_id"),
|
|
175
|
+
],
|
|
176
|
+
):
|
|
177
|
+
"""
|
|
178
|
+
Delete an existing job application entry.
|
|
179
|
+
"""
|
|
180
|
+
job = get_job(job_id)
|
|
181
|
+
if job is None:
|
|
182
|
+
typer.echo("Job application not found")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
delete_job(job)
|
|
186
|
+
typer.echo(f"Job #{job_id} deleted")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from datetime import date, datetime, time
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Date, DateTime, Integer, String, Time, UniqueConstraint
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
|
+
|
|
7
|
+
from .database import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Job(Base):
|
|
11
|
+
__tablename__ = "jobs"
|
|
12
|
+
|
|
13
|
+
id: Mapped[int] = mapped_column(
|
|
14
|
+
Integer,
|
|
15
|
+
primary_key=True,
|
|
16
|
+
index=True,
|
|
17
|
+
autoincrement=True,
|
|
18
|
+
)
|
|
19
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
20
|
+
DateTime, default=lambda: datetime.now()
|
|
21
|
+
)
|
|
22
|
+
role: Mapped[str] = mapped_column(String, nullable=False)
|
|
23
|
+
company: Mapped[str] = mapped_column(String, nullable=False)
|
|
24
|
+
location: Mapped[str] = mapped_column(String, nullable=False)
|
|
25
|
+
work_arrangement: Mapped[Literal["Onsite", "Hybrid", "Remote"]] = mapped_column(
|
|
26
|
+
String, nullable=False
|
|
27
|
+
)
|
|
28
|
+
status: Mapped[Literal["Applied", "Interview", "Hired", "Rejected"]] = (
|
|
29
|
+
mapped_column(String, nullable=False)
|
|
30
|
+
)
|
|
31
|
+
source_link: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
32
|
+
interview_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
|
33
|
+
interview_time: Mapped[time | None] = mapped_column(Time, nullable=True)
|
|
34
|
+
|
|
35
|
+
__table_args__ = (UniqueConstraint("role", "company", name="unq_role_company"),)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Literal, Sequence, Tuple
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from sqlalchemy import Row
|
|
8
|
+
|
|
9
|
+
from .constants import Colors
|
|
10
|
+
from .schemas import JobDetails, Status
|
|
11
|
+
|
|
12
|
+
JobRow = Tuple[
|
|
13
|
+
int,
|
|
14
|
+
datetime,
|
|
15
|
+
str,
|
|
16
|
+
str,
|
|
17
|
+
Literal["Applied", "Interview", "Hired", "Rejected"],
|
|
18
|
+
Literal["Onsite", "Hybrid", "Remote"],
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_jobs(
|
|
23
|
+
jobs: Sequence[Row[JobRow]],
|
|
24
|
+
):
|
|
25
|
+
table = Table(box=rich.box.SIMPLE)
|
|
26
|
+
|
|
27
|
+
table.add_column("ID", header_style=Colors.emerald.value)
|
|
28
|
+
table.add_column("Role", header_style=Colors.emerald.value)
|
|
29
|
+
table.add_column("Company", header_style=Colors.emerald.value)
|
|
30
|
+
table.add_column(
|
|
31
|
+
"Work arrangement", header_style=Colors.emerald.value, justify="center"
|
|
32
|
+
)
|
|
33
|
+
table.add_column("Status", header_style=Colors.emerald.value, justify="right")
|
|
34
|
+
table.add_column("Added on", header_style=Colors.emerald.value, justify="right")
|
|
35
|
+
|
|
36
|
+
for job_id, created_at, role, company, status, work_arrangement in jobs:
|
|
37
|
+
table.add_row(
|
|
38
|
+
f"[{Colors.gray.value}]{str(job_id)}",
|
|
39
|
+
f"[bold]{role}",
|
|
40
|
+
company,
|
|
41
|
+
work_arrangement,
|
|
42
|
+
render_status(status),
|
|
43
|
+
f"[{Colors.gray.value}]{created_at.strftime('%b %-d, %Y - %I:%M %p')}",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
rich.print(table)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_job(job_details_model: JobDetails):
|
|
50
|
+
job = job_details_model.model_dump(exclude_none=True)
|
|
51
|
+
|
|
52
|
+
table = Table(box=rich.box.SIMPLE, show_header=False)
|
|
53
|
+
table.add_column(style=f"bold {Colors.emerald.value}")
|
|
54
|
+
table.add_row("ID", str(job.get("id")))
|
|
55
|
+
table.add_row("Role", f"[bold]{job.get('role')}")
|
|
56
|
+
table.add_row("Company", job.get("company"))
|
|
57
|
+
table.add_row("Location", job.get("location"))
|
|
58
|
+
table.add_row("Work arrangement", job.get("work_arrangement"))
|
|
59
|
+
table.add_row("Status", render_status(job.get("status")))
|
|
60
|
+
table.add_row(
|
|
61
|
+
"Source link",
|
|
62
|
+
job.get("source_link", f"[{Colors.gray.value}]Unset"),
|
|
63
|
+
)
|
|
64
|
+
table.add_row(
|
|
65
|
+
"Interview date",
|
|
66
|
+
job.get("interview_date", f"[{Colors.gray.value}]Unset"),
|
|
67
|
+
)
|
|
68
|
+
table.add_row(
|
|
69
|
+
"Interview time",
|
|
70
|
+
job.get("interview_time", f"[{Colors.gray.value}]Unset"),
|
|
71
|
+
)
|
|
72
|
+
table.add_row(
|
|
73
|
+
"Added on",
|
|
74
|
+
job.get("created_at"),
|
|
75
|
+
)
|
|
76
|
+
rich.print(table)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def print_validation_errors(exception: ValidationError):
|
|
80
|
+
rich.print()
|
|
81
|
+
rich.print(f"[{Colors.red.value}]Validation error")
|
|
82
|
+
for error in exception.errors():
|
|
83
|
+
field: str = error["loc"][0]
|
|
84
|
+
field = (" ".join(field.split("_"))).capitalize()
|
|
85
|
+
message = error["msg"]
|
|
86
|
+
rich.print(f"{field} - [{Colors.gray.value}]{message}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_status(status: Status):
|
|
90
|
+
if status == "Applied":
|
|
91
|
+
return f"[yellow]{status}[/]"
|
|
92
|
+
if status == "Interview":
|
|
93
|
+
return f"[cyan]{status}[/]"
|
|
94
|
+
if status == "Hired":
|
|
95
|
+
return f"[green]{status}[/]"
|
|
96
|
+
if status == "Rejected":
|
|
97
|
+
return f"[red]{status}[/]"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from datetime import date, datetime, time
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, field_serializer
|
|
4
|
+
|
|
5
|
+
from .constants import Arrangement, Status
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JobBaseModel(BaseModel):
|
|
9
|
+
model_config = ConfigDict(
|
|
10
|
+
extra="ignore",
|
|
11
|
+
str_strip_whitespace=True,
|
|
12
|
+
str_min_length=1,
|
|
13
|
+
validate_default=True,
|
|
14
|
+
use_enum_values=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JobBase(JobBaseModel):
|
|
19
|
+
role: str
|
|
20
|
+
company: str
|
|
21
|
+
location: str
|
|
22
|
+
work_arrangement: Arrangement
|
|
23
|
+
status: Status
|
|
24
|
+
source_link: str | None = None
|
|
25
|
+
interview_date: date | None = None
|
|
26
|
+
interview_time: time | None = None
|
|
27
|
+
|
|
28
|
+
@field_serializer("interview_date", mode="plain")
|
|
29
|
+
def ser_interview_date(self, value: date | None):
|
|
30
|
+
if isinstance(value, date):
|
|
31
|
+
return value.strftime("%b %d, %Y")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
@field_serializer("interview_time", mode="plain")
|
|
35
|
+
def ser_interview_time(self, value: time | None):
|
|
36
|
+
if isinstance(value, time):
|
|
37
|
+
return value.strftime("%I:%M %p")
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class JobCreate(JobBase):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JobDetails(JobBase):
|
|
46
|
+
id: int
|
|
47
|
+
created_at: datetime
|
|
48
|
+
|
|
49
|
+
@field_serializer("created_at", mode="plain")
|
|
50
|
+
def ser_created_at(self, value: datetime):
|
|
51
|
+
return value.strftime("%b %-d, %Y - %I:%M %p")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class JobUpdate(JobBaseModel):
|
|
55
|
+
role: str | None = None
|
|
56
|
+
company: str | None = None
|
|
57
|
+
location: str | None = None
|
|
58
|
+
work_arrangement: Arrangement | None
|
|
59
|
+
status: Status | None = None
|
|
60
|
+
source_link: str | None = None
|
|
61
|
+
interview_date: date | None = None
|
|
62
|
+
interview_time: time | None = None
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from sqlalchemy import select
|
|
3
|
+
from sqlalchemy.exc import IntegrityError
|
|
4
|
+
|
|
5
|
+
from . import models
|
|
6
|
+
from .database import get_db_session
|
|
7
|
+
from .schemas import JobCreate, JobUpdate
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_job(job: JobCreate):
|
|
11
|
+
with get_db_session() as session:
|
|
12
|
+
try:
|
|
13
|
+
new_job = models.Job(
|
|
14
|
+
role=job.role,
|
|
15
|
+
company=job.company,
|
|
16
|
+
location=job.location,
|
|
17
|
+
work_arrangement=job.work_arrangement,
|
|
18
|
+
status=job.status,
|
|
19
|
+
source_link=job.source_link,
|
|
20
|
+
interview_date=job.interview_date,
|
|
21
|
+
interview_time=job.interview_time,
|
|
22
|
+
)
|
|
23
|
+
session.add(new_job)
|
|
24
|
+
session.commit()
|
|
25
|
+
session.refresh(new_job)
|
|
26
|
+
return new_job
|
|
27
|
+
except IntegrityError:
|
|
28
|
+
typer.echo("Job already exists")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_job(job_id: int):
|
|
33
|
+
with get_db_session() as session:
|
|
34
|
+
query = select(models.Job).where(models.Job.id == job_id)
|
|
35
|
+
job = session.execute(query).scalars().first()
|
|
36
|
+
return job
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_jobs(status: list[str], work_arrangement: list[str]):
|
|
40
|
+
with get_db_session() as session:
|
|
41
|
+
query = select(
|
|
42
|
+
models.Job.id,
|
|
43
|
+
models.Job.created_at,
|
|
44
|
+
models.Job.role,
|
|
45
|
+
models.Job.company,
|
|
46
|
+
models.Job.status,
|
|
47
|
+
models.Job.work_arrangement,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if status:
|
|
51
|
+
query = query.where(models.Job.status.in_(status))
|
|
52
|
+
|
|
53
|
+
if work_arrangement:
|
|
54
|
+
query = query.where(models.Job.work_arrangement.in_(work_arrangement))
|
|
55
|
+
|
|
56
|
+
jobs = session.execute(query).all()
|
|
57
|
+
return jobs
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def delete_job(job: models.Job):
|
|
61
|
+
with get_db_session() as session:
|
|
62
|
+
session.delete(job)
|
|
63
|
+
session.commit()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update_job(job_id: int, job_update: JobUpdate):
|
|
67
|
+
with get_db_session() as session:
|
|
68
|
+
job = session.get(models.Job, job_id)
|
|
69
|
+
if job is None:
|
|
70
|
+
typer.echo("Job application not found")
|
|
71
|
+
raise typer.Exit()
|
|
72
|
+
|
|
73
|
+
job_payload = job_update.model_dump(exclude_none=True)
|
|
74
|
+
for field, value in job_payload.items():
|
|
75
|
+
setattr(job, field, value)
|
|
76
|
+
|
|
77
|
+
session.commit()
|
|
78
|
+
session.refresh(job)
|
|
79
|
+
|
|
80
|
+
return job.id
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
|
+
|
|
6
|
+
from .print_helpers import print_validation_errors
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_model(model: type[BaseModel], payload: dict[str, Any]):
|
|
10
|
+
try:
|
|
11
|
+
validated_model = model.model_validate(payload)
|
|
12
|
+
return validated_model
|
|
13
|
+
except ValidationError as e:
|
|
14
|
+
print_validation_errors(e)
|
|
15
|
+
raise typer.Exit()
|