loop-agent-cli 0.1.0__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.
Files changed (84) hide show
  1. app/__init__.py +0 -0
  2. app/agents/__init__.py +0 -0
  3. app/agents/graph.py +40 -0
  4. app/agents/nodes.py +245 -0
  5. app/agents/state.py +19 -0
  6. app/api/__init__.py +0 -0
  7. app/api/candidates.py +49 -0
  8. app/api/dashboard.py +7 -0
  9. app/api/outreach.py +7 -0
  10. app/api/pipelines.py +58 -0
  11. app/api/positions.py +76 -0
  12. app/api/router.py +14 -0
  13. app/api/scheduler.py +7 -0
  14. app/api/skills.py +7 -0
  15. app/api/system.py +7 -0
  16. app/core/__init__.py +0 -0
  17. app/core/config.py +18 -0
  18. app/core/exception_handler.py +91 -0
  19. app/core/exceptions.py +33 -0
  20. app/core/logging.py +19 -0
  21. app/database/__init__.py +0 -0
  22. app/database/base.py +4 -0
  23. app/database/session.py +20 -0
  24. app/main.py +72 -0
  25. app/models/__init__.py +0 -0
  26. app/models/agent_run.py +18 -0
  27. app/models/candidate.py +28 -0
  28. app/models/node_log.py +18 -0
  29. app/models/outreach_log.py +16 -0
  30. app/models/pipeline.py +21 -0
  31. app/models/position.py +22 -0
  32. app/models/scheduler_job.py +16 -0
  33. app/models/skill.py +13 -0
  34. app/models/system_config.py +12 -0
  35. app/repositories/__init__.py +0 -0
  36. app/repositories/agent_run.py +74 -0
  37. app/repositories/candidate.py +84 -0
  38. app/repositories/node_log.py +57 -0
  39. app/repositories/outreach_log.py +60 -0
  40. app/repositories/pipeline.py +80 -0
  41. app/repositories/position.py +67 -0
  42. app/repositories/scheduler_job.py +74 -0
  43. app/schemas/__init__.py +0 -0
  44. app/schemas/agent_run.py +32 -0
  45. app/schemas/candidate.py +58 -0
  46. app/schemas/node_log.py +31 -0
  47. app/schemas/outreach_log.py +28 -0
  48. app/schemas/pipeline.py +34 -0
  49. app/schemas/position.py +49 -0
  50. app/schemas/scheduler_job.py +29 -0
  51. app/services/__init__.py +0 -0
  52. app/services/candidate.py +58 -0
  53. app/services/dashboard.py +230 -0
  54. app/services/email.py +116 -0
  55. app/services/health.py +105 -0
  56. app/services/pipeline.py +75 -0
  57. app/services/position.py +36 -0
  58. app/services/runner.py +292 -0
  59. app/services/scheduler.py +174 -0
  60. app/services/score.py +155 -0
  61. app/services/search.py +92 -0
  62. app/skills/base.py +30 -0
  63. app/skills/github.py +106 -0
  64. app/skills/registry.py +51 -0
  65. app/tests/__init__.py +3 -0
  66. app/tests/conftest.py +96 -0
  67. app/tests/generate_report.py +144 -0
  68. app/tests/test_candidates.py +158 -0
  69. app/tests/test_dashboard.py +27 -0
  70. app/tests/test_outreach.py +15 -0
  71. app/tests/test_pipelines.py +249 -0
  72. app/tests/test_positions.py +183 -0
  73. app/tests/test_scheduler.py +15 -0
  74. app/tests/test_skills.py +15 -0
  75. app/tests/test_system.py +35 -0
  76. app/utils/__init__.py +0 -0
  77. loop_agent_cli/__init__.py +5 -0
  78. loop_agent_cli/cli.py +728 -0
  79. loop_agent_cli/container.py +191 -0
  80. loop_agent_cli-0.1.0.dist-info/METADATA +202 -0
  81. loop_agent_cli-0.1.0.dist-info/RECORD +84 -0
  82. loop_agent_cli-0.1.0.dist-info/WHEEL +5 -0
  83. loop_agent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  84. loop_agent_cli-0.1.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,230 @@
1
+ from typing import Dict, Any, List
2
+ from datetime import datetime, timedelta
3
+ from app.repositories.position import PositionRepository
4
+ from app.repositories.agent_run import AgentRunRepository
5
+ from app.repositories.pipeline import PipelineRepository
6
+ from app.repositories.candidate import CandidateRepository
7
+ from app.repositories.node_log import NodeLogRepository
8
+ from app.models.position import Position
9
+ from app.models.agent_run import AgentRun
10
+ from app.models.pipeline import Pipeline
11
+ from app.models.candidate import Candidate
12
+ from app.models.node_log import NodeLog
13
+ import uuid
14
+
15
+
16
+ class DashboardService:
17
+ """
18
+ Service for retrieving dashboard metrics and statistics
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ position_repo: PositionRepository,
24
+ agent_run_repo: AgentRunRepository,
25
+ pipeline_repo: PipelineRepository,
26
+ candidate_repo: CandidateRepository,
27
+ node_log_repo: NodeLogRepository
28
+ ):
29
+ self.position_repo = position_repo
30
+ self.agent_run_repo = agent_run_repo
31
+ self.pipeline_repo = pipeline_repo
32
+ self.candidate_repo = candidate_repo
33
+ self.node_log_repo = node_log_repo
34
+
35
+ async def get_dashboard_summary(self) -> Dict[str, Any]:
36
+ """
37
+ Get the main dashboard summary metrics
38
+ """
39
+ # Get today's date for filtering
40
+ today_start = datetime.combine(datetime.today().date(), datetime.min.time())
41
+
42
+ # Count running positions
43
+ active_positions = await self.position_repo.get_all(status="active")
44
+ running_positions_count = len(active_positions)
45
+
46
+ # Count today's agent runs
47
+ all_runs = await self.agent_run_repo.get_all(skip=0, limit=1000)
48
+ today_runs = [run for run in all_runs if run.started_at.date() == datetime.today().date()]
49
+ today_loops = len(today_runs)
50
+
51
+ # Count today's candidates
52
+ all_candidates = await self.candidate_repo.get_all(skip=0, limit=1000)
53
+ today_candidates = [cand for cand in all_candidates if cand.created_at.date() == datetime.today().date()]
54
+ today_candidates_count = len(today_candidates)
55
+
56
+ # Count today's emails sent (from agent runs)
57
+ today_emails = sum(run.emails_sent for run in today_runs)
58
+
59
+ # Count recent errors
60
+ recent_errors = [run for run in all_runs if run.error and run.started_at.date() == datetime.today().date()]
61
+ today_errors = len(recent_errors)
62
+
63
+ # Count replies (candidates who have replied in pipeline)
64
+ replied_candidates = await self.pipeline_repo.get_by_status(None, "replied") # This would need to be filtered by date in practice
65
+
66
+ return {
67
+ "running_positions": running_positions_count,
68
+ "today_loops": today_loops,
69
+ "today_candidates": today_candidates_count,
70
+ "today_emails": today_emails,
71
+ "today_replies": len(replied_candidates), # Placeholder
72
+ "today_errors": today_errors
73
+ }
74
+
75
+ async def get_running_positions(self) -> List[Dict[str, Any]]:
76
+ """
77
+ Get information about currently running positions
78
+ """
79
+ active_positions = await self.position_repo.get_all(status="active")
80
+ result = []
81
+
82
+ for pos in active_positions:
83
+ # Get recent pipeline stats for this position
84
+ pipelines = await self.pipeline_repo.get_by_position(pos.id, skip=0, limit=100)
85
+
86
+ # Calculate stats
87
+ contacted_count = len([p for p in pipelines if p.status == "contacted"])
88
+
89
+ result.append({
90
+ "id": pos.id,
91
+ "title": pos.title,
92
+ "company": pos.company,
93
+ "status": pos.status,
94
+ "last_loop_at": pos.last_loop_at,
95
+ "next_loop_at": pos.next_loop_at,
96
+ "candidate_count": len(pipelines),
97
+ "contacted_count": contacted_count
98
+ })
99
+
100
+ return result
101
+
102
+ async def get_recent_activity(self) -> List[Dict[str, Any]]:
103
+ """
104
+ Get recent activity timeline
105
+ """
106
+ # Get recent agent runs and node logs
107
+ recent_runs = await self.agent_run_repo.get_all(skip=0, limit=20)
108
+ recent_logs = await self.node_log_repo.get_all(skip=0, limit=50)
109
+
110
+ activities = []
111
+
112
+ # Add agent runs to activities
113
+ for run in recent_runs[-10:]: # Last 10 runs
114
+ activities.append({
115
+ "time": run.started_at.strftime("%H:%M"),
116
+ "type": "loop",
117
+ "message": f"Loop completed for position {str(run.position_id)[:8]}",
118
+ "details": {
119
+ "candidates_found": run.candidates_found,
120
+ "candidates_added": run.candidates_added,
121
+ "emails_sent": run.emails_sent
122
+ }
123
+ })
124
+
125
+ # Add node logs to activities
126
+ for log in recent_logs[-10:]: # Last 10 logs
127
+ activities.append({
128
+ "time": log.started_at.strftime("%H:%M"),
129
+ "type": log.node_name,
130
+ "message": f"{log.node_name.capitalize()} node executed",
131
+ "details": {
132
+ "status": log.status,
133
+ "duration_ms": log.duration_ms
134
+ }
135
+ })
136
+
137
+ # Sort by time (most recent first)
138
+ activities.sort(key=lambda x: x["time"], reverse=True)
139
+
140
+ return activities[:20] # Return top 20
141
+
142
+ async def get_recent_errors(self) -> List[Dict[str, Any]]:
143
+ """
144
+ Get recent errors from agent runs and node logs
145
+ """
146
+ all_runs = await self.agent_run_repo.get_all(skip=0, limit=100)
147
+ all_logs = await self.node_log_repo.get_all(skip=0, limit=100)
148
+
149
+ errors = []
150
+
151
+ # Get errors from agent runs
152
+ for run in all_runs:
153
+ if run.error:
154
+ errors.append({
155
+ "time": run.started_at.strftime("%H:%M"),
156
+ "source": "agent_run",
157
+ "message": run.error,
158
+ "position_id": str(run.position_id)
159
+ })
160
+
161
+ # Get errors from node logs
162
+ for log in all_logs:
163
+ if log.error:
164
+ errors.append({
165
+ "time": log.started_at.strftime("%H:%M"),
166
+ "source": log.node_name,
167
+ "message": log.error,
168
+ "position_id": "unknown" # Would need to trace back to position
169
+ })
170
+
171
+ # Sort by time (most recent first)
172
+ errors.sort(key=lambda x: x["time"], reverse=True)
173
+
174
+ return errors[:10] # Return top 10
175
+
176
+ async def get_position_stats(self, position_id: uuid.UUID) -> Dict[str, Any]:
177
+ """
178
+ Get detailed statistics for a specific position
179
+ """
180
+ position = await self.position_repo.get_by_id(position_id)
181
+ if not position:
182
+ return {}
183
+
184
+ # Get pipelines for this position
185
+ pipelines = await self.pipeline_repo.get_by_position(position_id, skip=0, limit=1000)
186
+
187
+ # Count by status
188
+ status_counts = {}
189
+ for pipeline in pipelines:
190
+ status = pipeline.status
191
+ status_counts[status] = status_counts.get(status, 0) + 1
192
+
193
+ # Get related agent runs
194
+ runs = await self.agent_run_repo.get_by_position(position_id, skip=0, limit=100)
195
+
196
+ # Calculate metrics
197
+ total_candidates = len(pipelines)
198
+ contacted_count = status_counts.get("contacted", 0)
199
+ replied_count = status_counts.get("replied", 0)
200
+ total_emails_sent = sum(run.emails_sent for run in runs)
201
+
202
+ return {
203
+ "position": {
204
+ "id": position.id,
205
+ "title": position.title,
206
+ "company": position.company,
207
+ "status": position.status,
208
+ "created_at": position.created_at
209
+ },
210
+ "pipeline_stats": status_counts,
211
+ "metrics": {
212
+ "total_candidates": total_candidates,
213
+ "contacted": contacted_count,
214
+ "replied": replied_count,
215
+ "total_emails_sent": total_emails_sent,
216
+ "total_runs": len(runs)
217
+ },
218
+ "recent_runs": [
219
+ {
220
+ "id": run.id,
221
+ "started_at": run.started_at,
222
+ "finished_at": run.finished_at,
223
+ "duration_ms": run.duration_ms,
224
+ "candidates_found": run.candidates_found,
225
+ "candidates_added": run.candidates_added,
226
+ "emails_sent": run.emails_sent,
227
+ "status": run.status
228
+ } for run in runs[-5:] # Last 5 runs
229
+ ]
230
+ }
app/services/email.py ADDED
@@ -0,0 +1,116 @@
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ from app.core.config import settings
5
+ from app.core.exceptions import SMTPException
6
+ from app.repositories.outreach_log import OutreachLogRepository
7
+ from app.models.outreach_log import OutreachLog
8
+ from app.schemas.outreach_log import OutreachLogCreate
9
+ import asyncio
10
+ from typing import Optional
11
+ import uuid
12
+
13
+
14
+ class EmailService:
15
+ """
16
+ Service for sending emails to candidates
17
+ """
18
+
19
+ def __init__(self, outreach_log_repo: OutreachLogRepository):
20
+ self.outreach_log_repo = outreach_log_repo
21
+ self.smtp_host = settings.smtp_host
22
+ self.smtp_user = settings.smtp_user
23
+ self.smtp_password = settings.smtp_password
24
+ self.smtp_port = settings.smtp_port
25
+ self.sender_email = settings.EMAIL_FROM
26
+
27
+ async def send_email(self, recipient_email: str, subject: str, body: str, pipeline_id: uuid.UUID) -> bool:
28
+ """
29
+ Send an email to a candidate and log the result
30
+ """
31
+ # Create the outreach log entry
32
+ outreach_log_data = OutreachLogCreate(
33
+ pipeline_id=pipeline_id,
34
+ subject=subject,
35
+ body=body,
36
+ status="pending"
37
+ )
38
+
39
+ outreach_log = await self.outreach_log_repo.create(outreach_log_data)
40
+
41
+ try:
42
+ # Create message
43
+ msg = MIMEMultipart()
44
+ msg['From'] = self.sender_email
45
+ msg['To'] = recipient_email
46
+ msg['Subject'] = subject
47
+
48
+ # Add body to email
49
+ msg.attach(MIMEText(body, 'html'))
50
+
51
+ # Create SMTP session
52
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port)
53
+ server.starttls() # Enable encryption
54
+ server.login(self.smtp_user, self.smtp_password)
55
+
56
+ # Send email
57
+ text = msg.as_string()
58
+ server.sendmail(self.sender_email, recipient_email, text)
59
+ server.quit()
60
+
61
+ # Update outreach log status to sent
62
+ await self.outreach_log_repo.update_status(outreach_log.id, "sent")
63
+
64
+ return True
65
+
66
+ except Exception as e:
67
+ # Update outreach log status to failed
68
+ await self.outreach_log_repo.update_status_with_error(outreach_log.id, "failed", str(e))
69
+ raise SMTPException(f"Failed to send email: {str(e)}")
70
+
71
+ async def send_bulk_emails(self, recipients: list, subject: str, body: str) -> dict:
72
+ """
73
+ Send bulk emails and return statistics
74
+ """
75
+ stats = {
76
+ "total": len(recipients),
77
+ "sent": 0,
78
+ "failed": 0
79
+ }
80
+
81
+ for recipient in recipients:
82
+ try:
83
+ success = await self.send_email(recipient["email"], subject, body, recipient["pipeline_id"])
84
+ if success:
85
+ stats["sent"] += 1
86
+ else:
87
+ stats["failed"] += 1
88
+ except Exception:
89
+ stats["failed"] += 1
90
+
91
+ # Small delay to avoid overwhelming the SMTP server
92
+ await asyncio.sleep(1)
93
+
94
+ return stats
95
+
96
+ def generate_email_template(self, candidate_name: str, position_title: str, company_name: str) -> str:
97
+ """
98
+ Generate a personalized email template
99
+ """
100
+ template = f"""
101
+ <html>
102
+ <body>
103
+ <p>Dear {candidate_name},</p>
104
+
105
+ <p>I came across your profile on GitHub and noticed your expertise in technologies that align with our current opening for a {position_title} at {company_name}. Given your impressive contributions, I thought this opportunity might interest you.</p>
106
+
107
+ <p>We are looking for someone with skills in {position_title.split()[0] if position_title else 'software development'} and would love to discuss how you could contribute to our team.</p>
108
+
109
+ <p>Would you be interested in learning more about this opportunity?</p>
110
+
111
+ <p>Best regards,<br/>
112
+ Recruiting Loop Agent</p>
113
+ </body>
114
+ </html>
115
+ """
116
+ return template
app/services/health.py ADDED
@@ -0,0 +1,105 @@
1
+ from typing import Dict, Any
2
+ from app.database.session import engine
3
+ from sqlalchemy import text
4
+ import httpx
5
+ import asyncio
6
+ from app.core.config import settings
7
+
8
+
9
+ class HealthService:
10
+ """
11
+ Service for checking the health of various system components
12
+ """
13
+
14
+ async def check_database(self) -> Dict[str, Any]:
15
+ """
16
+ Check database connectivity
17
+ """
18
+ try:
19
+ async with engine.begin() as conn:
20
+ await conn.execute(text("SELECT 1"))
21
+ return {"status": "connected", "message": "Database is accessible"}
22
+ except Exception as e:
23
+ return {"status": "error", "message": f"Database connection failed: {str(e)}"}
24
+
25
+ async def check_scheduler(self) -> Dict[str, Any]:
26
+ """
27
+ Check scheduler status
28
+ """
29
+ # For now, just return a placeholder
30
+ # In a real implementation, this would check the actual scheduler status
31
+ return {"status": "running", "message": "Scheduler is operational"}
32
+
33
+ async def check_github(self) -> Dict[str, Any]:
34
+ """
35
+ Check GitHub API connectivity
36
+ """
37
+ try:
38
+ if not settings.github_token:
39
+ return {"status": "warning", "message": "GitHub token not configured"}
40
+
41
+ headers = {
42
+ "Authorization": f"token {settings.github_token}",
43
+ "Accept": "application/vnd.github.v3+json"
44
+ }
45
+
46
+ async with httpx.AsyncClient() as client:
47
+ response = await client.get("https://api.github.com/user", headers=headers)
48
+
49
+ if response.status_code == 200:
50
+ return {"status": "connected", "message": "GitHub API is accessible"}
51
+ else:
52
+ return {"status": "error", "message": f"GitHub API returned status {response.status_code}"}
53
+ except Exception as e:
54
+ return {"status": "error", "message": f"GitHub API connection failed: {str(e)}"}
55
+
56
+ async def check_smtp(self) -> Dict[str, Any]:
57
+ """
58
+ Check SMTP connectivity
59
+ """
60
+ try:
61
+ if not settings.smtp_host or not settings.smtp_user or not settings.smtp_password:
62
+ return {"status": "warning", "message": "SMTP credentials not fully configured"}
63
+
64
+ # For now, just check if the config is available
65
+ # In a real implementation, we'd try to connect to the SMTP server
66
+ return {"status": "configured", "message": "SMTP is configured"}
67
+ except Exception as e:
68
+ return {"status": "error", "message": f"SMTP configuration error: {str(e)}"}
69
+
70
+ async def get_full_health_status(self) -> Dict[str, Any]:
71
+ """
72
+ Get comprehensive health status of all system components
73
+ """
74
+ # Run all health checks concurrently
75
+ db_task = self.check_database()
76
+ scheduler_task = self.check_scheduler()
77
+ github_task = self.check_github()
78
+ smtp_task = self.check_smtp()
79
+
80
+ results = await asyncio.gather(
81
+ db_task, scheduler_task, github_task, smtp_task,
82
+ return_exceptions=True
83
+ )
84
+
85
+ # Process results
86
+ health_status = {
87
+ "database": results[0] if not isinstance(results[0], Exception) else {"status": "error", "message": str(results[0])},
88
+ "scheduler": results[1] if not isinstance(results[1], Exception) else {"status": "error", "message": str(results[1])},
89
+ "github": results[2] if not isinstance(results[2], Exception) else {"status": "error", "message": str(results[2])},
90
+ "smtp": results[3] if not isinstance(results[3], Exception) else {"status": "error", "message": str(results[3])},
91
+ }
92
+
93
+ # Determine overall status
94
+ overall_status = "healthy"
95
+ for component, status_info in health_status.items():
96
+ if status_info["status"] == "error":
97
+ overall_status = "error"
98
+ break
99
+ elif status_info["status"] == "warning" and overall_status != "error":
100
+ overall_status = "warning"
101
+
102
+ health_status["overall"] = overall_status
103
+ health_status["timestamp"] = asyncio.get_event_loop().time()
104
+
105
+ return health_status
@@ -0,0 +1,75 @@
1
+ from typing import List, Optional
2
+ from app.repositories.pipeline import PipelineRepository
3
+ from app.schemas.pipeline import PipelineCreate, PipelineUpdate, Pipeline
4
+ from app.core.exceptions import PipelineNotFoundException
5
+ import uuid
6
+
7
+ class PipelineService:
8
+ def __init__(self, pipeline_repo: PipelineRepository):
9
+ self.pipeline_repo = pipeline_repo
10
+
11
+ async def create_pipeline(self, pipeline_data: PipelineCreate) -> Pipeline:
12
+ # Check if pipeline already exists for this position and candidate
13
+ existing_pipeline = await self.pipeline_repo.get_by_position_and_candidate(
14
+ pipeline_data.position_id,
15
+ pipeline_data.candidate_id
16
+ )
17
+
18
+ if existing_pipeline:
19
+ # Return existing pipeline instead of creating duplicate
20
+ return existing_pipeline
21
+
22
+ return await self.pipeline_repo.create(pipeline_data)
23
+
24
+ async def get_pipeline_by_id(self, pipeline_id: uuid.UUID) -> Pipeline:
25
+ pipeline = await self.pipeline_repo.get_by_id(pipeline_id)
26
+ if not pipeline:
27
+ raise PipelineNotFoundException(f"Pipeline with id {pipeline_id} not found")
28
+ return pipeline
29
+
30
+ async def get_pipeline_by_position_and_candidate(self, position_id: uuid.UUID, candidate_id: uuid.UUID) -> Optional[Pipeline]:
31
+ return await self.pipeline_repo.get_by_position_and_candidate(position_id, candidate_id)
32
+
33
+ async def get_pipelines_by_position(self, position_id: uuid.UUID, skip: int = 0, limit: int = 100) -> List[Pipeline]:
34
+ return await self.pipeline_repo.get_by_position(position_id, skip, limit)
35
+
36
+ async def get_pipelines_by_status(self, position_id: uuid.UUID, status: str) -> List[Pipeline]:
37
+ return await self.pipeline_repo.get_by_status(position_id, status)
38
+
39
+ async def get_all_pipelines(self, skip: int = 0, limit: int = 100) -> List[Pipeline]:
40
+ return await self.pipeline_repo.get_all(skip, limit)
41
+
42
+ async def update_pipeline(self, pipeline_id: uuid.UUID, pipeline_data: PipelineUpdate) -> Optional[Pipeline]:
43
+ return await self.pipeline_repo.update(pipeline_id, pipeline_data)
44
+
45
+ async def update_pipeline_status(self, pipeline_id: uuid.UUID, status: str) -> Optional[Pipeline]:
46
+ return await self.pipeline_repo.update_status(pipeline_id, status)
47
+
48
+ async def delete_pipeline(self, pipeline_id: uuid.UUID) -> bool:
49
+ return await self.pipeline_repo.delete(pipeline_id)
50
+
51
+ async def move_to_next_stage(self, pipeline_id: uuid.UUID) -> Optional[Pipeline]:
52
+ """
53
+ Move pipeline to the next stage in the recruitment process
54
+ discovered -> contacted -> replied -> interview -> offer -> rejected
55
+ """
56
+ pipeline = await self.get_pipeline_by_id(pipeline_id)
57
+ if pipeline:
58
+ current_status = pipeline.status
59
+ if current_status == "discovered":
60
+ next_status = "contacted"
61
+ elif current_status == "contacted":
62
+ next_status = "replied"
63
+ elif current_status == "replied":
64
+ next_status = "interview"
65
+ elif current_status == "interview":
66
+ next_status = "offer"
67
+ elif current_status == "offer":
68
+ next_status = "rejected"
69
+ else:
70
+ next_status = current_status
71
+
72
+ if current_status != next_status:
73
+ return await self.update_pipeline_status(pipeline_id, next_status)
74
+
75
+ return pipeline
@@ -0,0 +1,36 @@
1
+ from typing import List, Optional
2
+ from app.repositories.position import PositionRepository
3
+ from app.schemas.position import PositionCreate, PositionUpdate, Position
4
+ from app.core.exceptions import PositionNotFoundException
5
+ import uuid
6
+
7
+ class PositionService:
8
+ def __init__(self, position_repo: PositionRepository):
9
+ self.position_repo = position_repo
10
+
11
+ async def create_position(self, position_data: PositionCreate) -> Position:
12
+ return await self.position_repo.create(position_data)
13
+
14
+ async def get_position_by_id(self, position_id: uuid.UUID) -> Position:
15
+ position = await self.position_repo.get_by_id(position_id)
16
+ if not position:
17
+ raise PositionNotFoundException(f"Position with id {position_id} not found")
18
+ return position
19
+
20
+ async def get_all_positions(self, skip: int = 0, limit: int = 100, status: Optional[str] = None) -> List[Position]:
21
+ return await self.position_repo.get_all(skip, limit, status)
22
+
23
+ async def update_position(self, position_id: uuid.UUID, position_data: PositionUpdate) -> Optional[Position]:
24
+ return await self.position_repo.update(position_id, position_data)
25
+
26
+ async def delete_position(self, position_id: uuid.UUID) -> bool:
27
+ return await self.position_repo.delete(position_id)
28
+
29
+ async def pause_position(self, position_id: uuid.UUID) -> Optional[Position]:
30
+ return await self.position_repo.update_status(position_id, "paused")
31
+
32
+ async def resume_position(self, position_id: uuid.UUID) -> Optional[Position]:
33
+ return await self.position_repo.update_status(position_id, "active")
34
+
35
+ async def close_position(self, position_id: uuid.UUID) -> Optional[Position]:
36
+ return await self.position_repo.update_status(position_id, "closed")