esdd-client 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.
- esdd_client-0.1.0/PKG-INFO +7 -0
- esdd_client-0.1.0/README.md +79 -0
- esdd_client-0.1.0/app/src/application/__init__.py +0 -0
- esdd_client-0.1.0/app/src/application/services/auth_service.py +62 -0
- esdd_client-0.1.0/app/src/application/services/task_service.py +44 -0
- esdd_client-0.1.0/app/src/domain/__init__.py +0 -0
- esdd_client-0.1.0/app/src/domain/repositories/auth_repository.py +13 -0
- esdd_client-0.1.0/app/src/domain/repositories/task_repository.py +62 -0
- esdd_client-0.1.0/app/src/infrastructure/__init__.py +0 -0
- esdd_client-0.1.0/app/src/infrastructure/repositories/git_oauth_repository.py +35 -0
- esdd_client-0.1.0/app/src/infrastructure/repositories/mpds_oauth_repository.py +38 -0
- esdd_client-0.1.0/app/src/infrastructure/repositories/task_repository_impl.py +130 -0
- esdd_client-0.1.0/app/src/presentation/__init__.py +0 -0
- esdd_client-0.1.0/app/src/presentation/cli.py +22 -0
- esdd_client-0.1.0/app/src/presentation/routers/auth.py +101 -0
- esdd_client-0.1.0/app/src/presentation/routers/task.py +232 -0
- esdd_client-0.1.0/app/src/presentation/utils.py +75 -0
- esdd_client-0.1.0/esdd_client.egg-info/PKG-INFO +7 -0
- esdd_client-0.1.0/esdd_client.egg-info/SOURCES.txt +23 -0
- esdd_client-0.1.0/esdd_client.egg-info/dependency_links.txt +1 -0
- esdd_client-0.1.0/esdd_client.egg-info/entry_points.txt +2 -0
- esdd_client-0.1.0/esdd_client.egg-info/requires.txt +4 -0
- esdd_client-0.1.0/esdd_client.egg-info/top_level.txt +1 -0
- esdd_client-0.1.0/pyproject.toml +21 -0
- esdd_client-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ESDD Client
|
|
2
|
+
|
|
3
|
+
## installation
|
|
4
|
+
At first, install requirements:
|
|
5
|
+
```
|
|
6
|
+
pip3 install -e .
|
|
7
|
+
```
|
|
8
|
+
Usage:
|
|
9
|
+
```
|
|
10
|
+
esdd <arguments>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic commands
|
|
14
|
+
|
|
15
|
+
### Authentication via Github
|
|
16
|
+
``` bash
|
|
17
|
+
# Get authorization URL
|
|
18
|
+
esdd auth github
|
|
19
|
+
|
|
20
|
+
# Copy URL to browser, authorize, get code
|
|
21
|
+
|
|
22
|
+
# Exchange code for token
|
|
23
|
+
esdd auth token github <your_code>
|
|
24
|
+
|
|
25
|
+
# set token
|
|
26
|
+
esdd auth set-token <token>
|
|
27
|
+
|
|
28
|
+
# Check token
|
|
29
|
+
esdd auth info
|
|
30
|
+
```
|
|
31
|
+
### Working with tasks
|
|
32
|
+
``` bash
|
|
33
|
+
# Create task
|
|
34
|
+
esdd task create --question "Annotate dataset" --bid 50
|
|
35
|
+
|
|
36
|
+
# List tasks
|
|
37
|
+
esdd task list
|
|
38
|
+
esdd task list --status pending --limit 5
|
|
39
|
+
|
|
40
|
+
# Get task details
|
|
41
|
+
esdd task get 123
|
|
42
|
+
|
|
43
|
+
# Assign to user
|
|
44
|
+
esdd task assign 123 456
|
|
45
|
+
|
|
46
|
+
# Start task
|
|
47
|
+
esdd task start 123
|
|
48
|
+
|
|
49
|
+
# Finalize with data
|
|
50
|
+
esdd task update 123 --json_data '{"result": "completed"}'
|
|
51
|
+
esdd task finalize 123
|
|
52
|
+
|
|
53
|
+
# Approve/Reject
|
|
54
|
+
esdd task approve 123
|
|
55
|
+
esdd task reject 123
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Complete workflow example
|
|
59
|
+
``` bash
|
|
60
|
+
# 1. Login as admin
|
|
61
|
+
esdd auth set-token <admin_jwt>
|
|
62
|
+
|
|
63
|
+
# 2. Create and assign task
|
|
64
|
+
esdd task create --question "Validate data" --assign-to 789 --bid 100
|
|
65
|
+
|
|
66
|
+
# 3. Login as executor
|
|
67
|
+
esdd auth logout
|
|
68
|
+
esdd auth set-token <executor_jwt>
|
|
69
|
+
|
|
70
|
+
# 4. Execute task
|
|
71
|
+
esdd task list
|
|
72
|
+
esdd task start <task_id>
|
|
73
|
+
esdd task finalize <task_id>
|
|
74
|
+
|
|
75
|
+
# 5. Return as admin
|
|
76
|
+
esdd auth logout
|
|
77
|
+
esdd auth set-token <admin_jwt>
|
|
78
|
+
esdd task approve <task_id>
|
|
79
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from app.src.infrastructure.repositories.git_oauth_repository import GitOAuthRepository
|
|
4
|
+
from app.src.infrastructure.repositories.mpds_oauth_repository import (
|
|
5
|
+
MPDSOAuthRepository,
|
|
6
|
+
)
|
|
7
|
+
from app.src.infrastructure.repositories.task_repository_impl import TaskRepositoryImpl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthService:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
git_repo: Optional[GitOAuthRepository] = None,
|
|
14
|
+
mpds_repo: Optional[MPDSOAuthRepository] = None,
|
|
15
|
+
task_repo: Optional[TaskRepositoryImpl] = None,
|
|
16
|
+
api_url: Optional[str] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
self.api_url = api_url or "http://localhost:8000"
|
|
19
|
+
self.git = git_repo or GitOAuthRepository(api_url=self.api_url)
|
|
20
|
+
self.mpds = mpds_repo or MPDSOAuthRepository(api_url=self.api_url)
|
|
21
|
+
self.task_repo = task_repo
|
|
22
|
+
self._current_token: Optional[str] = None
|
|
23
|
+
self._current_user_id: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
def set_token(self, token: str, user_id: Optional[int] = None) -> None:
|
|
26
|
+
self._current_token = token
|
|
27
|
+
self._current_user_id = user_id
|
|
28
|
+
if self.task_repo:
|
|
29
|
+
self.task_repo.set_token(token)
|
|
30
|
+
|
|
31
|
+
def get_current_token(self) -> Optional[str]:
|
|
32
|
+
return self._current_token
|
|
33
|
+
|
|
34
|
+
def get_current_user_id(self) -> Optional[int]:
|
|
35
|
+
return self._current_user_id
|
|
36
|
+
|
|
37
|
+
def get_auth_url(
|
|
38
|
+
self, provider: str = "github", redirect_url: Optional[str] = None
|
|
39
|
+
) -> str:
|
|
40
|
+
if provider == "github":
|
|
41
|
+
return self.git.generate_auth_url(redirect_url)
|
|
42
|
+
elif provider == "mpds":
|
|
43
|
+
return self.mpds.generate_auth_url(redirect_url)
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
46
|
+
|
|
47
|
+
async def get_task_repository(self) -> TaskRepositoryImpl:
|
|
48
|
+
if not self._current_token:
|
|
49
|
+
raise ValueError("No token available. Please authenticate first.")
|
|
50
|
+
|
|
51
|
+
if not self.task_repo:
|
|
52
|
+
self.task_repo = TaskRepositoryImpl(
|
|
53
|
+
api_url=self.api_url, token=self._current_token
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return self.task_repo
|
|
57
|
+
|
|
58
|
+
def clear_auth(self) -> None:
|
|
59
|
+
self._current_token = None
|
|
60
|
+
self._current_user_id = None
|
|
61
|
+
if self.task_repo:
|
|
62
|
+
self.task_repo.set_token(None)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from app.src.domain.repositories.task_repository import TaskRepository
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskService:
|
|
7
|
+
def __init__(self, task_repository: TaskRepository):
|
|
8
|
+
self.task_repository = task_repository
|
|
9
|
+
|
|
10
|
+
async def create_task(self, task_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
11
|
+
return await self.task_repository.create_task(task_data)
|
|
12
|
+
|
|
13
|
+
async def get_tasks(
|
|
14
|
+
self,
|
|
15
|
+
page: int,
|
|
16
|
+
per_page: int,
|
|
17
|
+
) -> List[Dict[str, Any]]:
|
|
18
|
+
return await self.task_repository.get_tasks(page, per_page)
|
|
19
|
+
|
|
20
|
+
async def update_task(
|
|
21
|
+
self, task_id: int, task_data: Dict[str, Any]
|
|
22
|
+
) -> Dict[str, Any]:
|
|
23
|
+
return await self.task_repository.update_task(task_id, task_data)
|
|
24
|
+
|
|
25
|
+
async def get_task(self, task_id: int) -> Dict[str, Any]:
|
|
26
|
+
return await self.task_repository.get_task(task_id)
|
|
27
|
+
|
|
28
|
+
async def delete_task(self, task_id: int) -> Dict[str, Any]:
|
|
29
|
+
return await self.task_repository.delete_task(task_id)
|
|
30
|
+
|
|
31
|
+
async def assign_task(self, task_id: int, user_id: int) -> Dict[str, Any]:
|
|
32
|
+
return await self.task_repository.assign_task(task_id, user_id)
|
|
33
|
+
|
|
34
|
+
async def start_task(self, task_id: int) -> Dict[str, Any]:
|
|
35
|
+
return await self.task_repository.start_task(task_id)
|
|
36
|
+
|
|
37
|
+
async def finalize_task(self, task_id: int) -> Dict[str, Any]:
|
|
38
|
+
return await self.task_repository.finalize_task(task_id)
|
|
39
|
+
|
|
40
|
+
async def approve_task(self, task_id: int) -> Dict[str, Any]:
|
|
41
|
+
return await self.task_repository.approve_task(task_id)
|
|
42
|
+
|
|
43
|
+
async def reject_task(self, task_id: int) -> Dict[str, Any]:
|
|
44
|
+
return await self.task_repository.reject_task(task_id)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthRepository(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
def generate_auth_url(self, *args, **kwargs) -> str:
|
|
7
|
+
"""Generate authentication URL"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def get_token(self, *args, **kwargs) -> str:
|
|
12
|
+
"""Get authentication token"""
|
|
13
|
+
pass
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TaskRepository(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
async def create_task(self, task_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
8
|
+
"""Create a new task"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def get_tasks(
|
|
13
|
+
self,
|
|
14
|
+
skip: int = 0,
|
|
15
|
+
limit: int = 100,
|
|
16
|
+
status: Optional[str] = None,
|
|
17
|
+
assignee_id: Optional[int] = None,
|
|
18
|
+
) -> List[Dict[str, Any]]:
|
|
19
|
+
"""Get list of tasks with optional filtering"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def update_task(
|
|
24
|
+
self, task_id: int, task_data: Dict[str, Any]
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
"""Update an existing task"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def get_task(self, task_id: int) -> Dict[str, Any]:
|
|
31
|
+
"""Get detailed information about a specific task"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def delete_task(self, task_id: int) -> Dict[str, Any]:
|
|
36
|
+
"""Delete a task"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def assign_task(self, task_id: int, user_id: int) -> Dict[str, Any]:
|
|
41
|
+
"""Assign a task to a user"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def start_task(self, task_id: int) -> Dict[str, Any]:
|
|
46
|
+
"""Mark task as started"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def finalize_task(self, task_id: int) -> Dict[str, Any]:
|
|
51
|
+
"""Mark task as finalized/completed"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def approve_task(self, task_id: int) -> Dict[str, Any]:
|
|
56
|
+
"""Approve a completed task"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
async def reject_task(self, task_id: int) -> Dict[str, Any]:
|
|
61
|
+
"""Reject a completed task"""
|
|
62
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from app.src.domain.repositories.auth_repository import AuthRepository
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GitOAuthRepository(AuthRepository):
|
|
11
|
+
def __init__(self, api_url: str = None):
|
|
12
|
+
self.api_url = api_url or f"{os.getenv('API_URL').rstrip('/')}/api/v1"
|
|
13
|
+
|
|
14
|
+
def generate_auth_url(self, redirect_url: str = None) -> str:
|
|
15
|
+
params = {}
|
|
16
|
+
if redirect_url:
|
|
17
|
+
params["redirect"] = redirect_url
|
|
18
|
+
query_string = f"?{urlencode(params)}" if params else ""
|
|
19
|
+
return f"{self.api_url}/auth/github{query_string}"
|
|
20
|
+
|
|
21
|
+
async def get_token(
|
|
22
|
+
self, authorization_code: str, redirect_uri: str = None
|
|
23
|
+
) -> Dict:
|
|
24
|
+
base_url = os.getenv("API_URL").rstrip("/")
|
|
25
|
+
async with httpx.AsyncClient() as client:
|
|
26
|
+
response = await client.post(
|
|
27
|
+
f"{self.api_url}/auth/exchange",
|
|
28
|
+
json={
|
|
29
|
+
"provider": "github",
|
|
30
|
+
"code": authorization_code,
|
|
31
|
+
"redirect_uri": redirect_uri or f"{base_url}/auth/callback/github",
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
return response.json()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from urllib.parse import urlencode
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MPDSOAuthRepository:
|
|
9
|
+
def __init__(self, api_url: str = None):
|
|
10
|
+
self.api_url = (
|
|
11
|
+
api_url or f"{os.getenv('API_URL', 'http://localhost:8000').rstrip('/')}/api/v1"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def generate_auth_url(self, redirect_url: str = None) -> str:
|
|
15
|
+
params = {}
|
|
16
|
+
if redirect_url:
|
|
17
|
+
params["redirect"] = redirect_url
|
|
18
|
+
query_string = f"?{urlencode(params)}" if params else ""
|
|
19
|
+
return f"{self.api_url}/auth/mpds{query_string}"
|
|
20
|
+
|
|
21
|
+
async def get_token(
|
|
22
|
+
self, authorization_code: str, redirect_uri: str = None
|
|
23
|
+
) -> Dict:
|
|
24
|
+
base_url = os.getenv("BASE_URL", "http://localhost:8000")
|
|
25
|
+
actual_redirect_uri = redirect_uri or f"{base_url}/auth/callback/mpds"
|
|
26
|
+
|
|
27
|
+
async with httpx.AsyncClient() as client:
|
|
28
|
+
response = await client.post(
|
|
29
|
+
f"{self.api_url}/auth/exchange",
|
|
30
|
+
json={
|
|
31
|
+
"provider": "mpds",
|
|
32
|
+
"code": authorization_code,
|
|
33
|
+
"redirect_uri": actual_redirect_uri,
|
|
34
|
+
},
|
|
35
|
+
timeout=30.0,
|
|
36
|
+
)
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return response.json()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaskRepositoryImpl:
|
|
8
|
+
def __init__(
|
|
9
|
+
self, api_url: Optional[str] = None, token: Optional[str] = None
|
|
10
|
+
) -> None:
|
|
11
|
+
self.api_url = api_url or os.getenv("API_URL", "http://localhost:8000")
|
|
12
|
+
if self.api_url:
|
|
13
|
+
self.api_url = f"{self.api_url.rstrip('/')}/api/v1"
|
|
14
|
+
self.token = token
|
|
15
|
+
|
|
16
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
17
|
+
headers = {"Content-Type": "application/json"}
|
|
18
|
+
if self.token:
|
|
19
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
20
|
+
return headers
|
|
21
|
+
|
|
22
|
+
def set_token(self, token: str) -> None:
|
|
23
|
+
self.token = token
|
|
24
|
+
|
|
25
|
+
async def create_task(self, task_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
26
|
+
async with httpx.AsyncClient() as client:
|
|
27
|
+
response = await client.post(
|
|
28
|
+
f"{self.api_url}/tasks/",
|
|
29
|
+
json=task_data,
|
|
30
|
+
headers=self._get_headers(),
|
|
31
|
+
timeout=30.0,
|
|
32
|
+
)
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
async def get_tasks(
|
|
36
|
+
self,
|
|
37
|
+
page: int,
|
|
38
|
+
per_page: int,
|
|
39
|
+
# status: Optional[str] = None,
|
|
40
|
+
# assignee_id: Optional[int] = None,
|
|
41
|
+
) -> List[Dict[str, Any]]:
|
|
42
|
+
params = {
|
|
43
|
+
"page": page,
|
|
44
|
+
"per_page": per_page,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async with httpx.AsyncClient() as client:
|
|
48
|
+
response = await client.get(
|
|
49
|
+
f"{self.api_url}/tasks/",
|
|
50
|
+
params=params,
|
|
51
|
+
headers=self._get_headers(),
|
|
52
|
+
timeout=30.0,
|
|
53
|
+
)
|
|
54
|
+
data = response.json()
|
|
55
|
+
return data.get("items", [])
|
|
56
|
+
|
|
57
|
+
async def get_task(self, task_id: int) -> Dict[str, Any]:
|
|
58
|
+
async with httpx.AsyncClient() as client:
|
|
59
|
+
response = await client.get(
|
|
60
|
+
f"{self.api_url}/tasks/{task_id}/",
|
|
61
|
+
headers=self._get_headers(),
|
|
62
|
+
timeout=30.0,
|
|
63
|
+
)
|
|
64
|
+
return response.json()
|
|
65
|
+
|
|
66
|
+
async def update_task(
|
|
67
|
+
self, task_id: int, task_data: Dict[str, Any]
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
async with httpx.AsyncClient() as client:
|
|
70
|
+
response = await client.put(
|
|
71
|
+
f"{self.api_url}/tasks/{task_id}/",
|
|
72
|
+
json=task_data,
|
|
73
|
+
headers=self._get_headers(),
|
|
74
|
+
timeout=30.0,
|
|
75
|
+
)
|
|
76
|
+
return response.json()
|
|
77
|
+
|
|
78
|
+
async def delete_task(self, task_id: int) -> Dict[str, Any]:
|
|
79
|
+
async with httpx.AsyncClient() as client:
|
|
80
|
+
response = await client.delete(
|
|
81
|
+
f"{self.api_url}/tasks/{task_id}/",
|
|
82
|
+
headers=self._get_headers(),
|
|
83
|
+
timeout=30.0,
|
|
84
|
+
)
|
|
85
|
+
return response.json()
|
|
86
|
+
|
|
87
|
+
async def assign_task(self, task_id: int, user_id: int) -> Dict[str, Any]:
|
|
88
|
+
async with httpx.AsyncClient() as client:
|
|
89
|
+
response = await client.put(
|
|
90
|
+
f"{self.api_url}/tasks/{task_id}/assign/{user_id}/",
|
|
91
|
+
headers=self._get_headers(),
|
|
92
|
+
timeout=30.0,
|
|
93
|
+
)
|
|
94
|
+
return response.json()
|
|
95
|
+
|
|
96
|
+
async def start_task(self, task_id: int) -> Dict[str, Any]:
|
|
97
|
+
async with httpx.AsyncClient() as client:
|
|
98
|
+
response = await client.put(
|
|
99
|
+
f"{self.api_url}/tasks/{task_id}/start/",
|
|
100
|
+
headers=self._get_headers(),
|
|
101
|
+
timeout=30.0,
|
|
102
|
+
)
|
|
103
|
+
return response.json()
|
|
104
|
+
|
|
105
|
+
async def finalize_task(self, task_id: int) -> Dict[str, Any]:
|
|
106
|
+
async with httpx.AsyncClient() as client:
|
|
107
|
+
response = await client.put(
|
|
108
|
+
f"{self.api_url}/tasks/{task_id}/finalize/",
|
|
109
|
+
headers=self._get_headers(),
|
|
110
|
+
timeout=30.0,
|
|
111
|
+
)
|
|
112
|
+
return response.json()
|
|
113
|
+
|
|
114
|
+
async def approve_task(self, task_id: int) -> Dict[str, Any]:
|
|
115
|
+
async with httpx.AsyncClient() as client:
|
|
116
|
+
response = await client.put(
|
|
117
|
+
f"{self.api_url}/tasks/{task_id}/approve/",
|
|
118
|
+
headers=self._get_headers(),
|
|
119
|
+
timeout=30.0,
|
|
120
|
+
)
|
|
121
|
+
return response.json()
|
|
122
|
+
|
|
123
|
+
async def reject_task(self, task_id: int) -> Dict[str, Any]:
|
|
124
|
+
async with httpx.AsyncClient() as client:
|
|
125
|
+
response = await client.put(
|
|
126
|
+
f"{self.api_url}/tasks/{task_id}/reject/",
|
|
127
|
+
headers=self._get_headers(),
|
|
128
|
+
timeout=30.0,
|
|
129
|
+
)
|
|
130
|
+
return response.json()
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
import click
|
|
3
|
+
|
|
4
|
+
load_dotenv(".config")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def cli():
|
|
9
|
+
"""Global CLI Group"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import app.src.presentation.routers.auth
|
|
14
|
+
import app.src.presentation.routers.task
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
cli(prog_name="esdd")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import jwt
|
|
6
|
+
|
|
7
|
+
from app.src.infrastructure.repositories.git_oauth_repository import GitOAuthRepository
|
|
8
|
+
from app.src.infrastructure.repositories.mpds_oauth_repository import (
|
|
9
|
+
MPDSOAuthRepository,
|
|
10
|
+
)
|
|
11
|
+
from app.src.presentation.cli import cli
|
|
12
|
+
from app.src.presentation.utils import TokenManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.group()
|
|
16
|
+
def auth():
|
|
17
|
+
"""Authentication commands"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@auth.command()
|
|
22
|
+
@click.option("--redirect", help="Redirect URL")
|
|
23
|
+
def github(redirect):
|
|
24
|
+
"""GitHub authentication"""
|
|
25
|
+
git_repo = GitOAuthRepository()
|
|
26
|
+
url = git_repo.generate_auth_url(redirect)
|
|
27
|
+
click.echo("GitHub Auth URL:")
|
|
28
|
+
click.echo(url)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@auth.command()
|
|
32
|
+
@click.option("--redirect", help="Redirect URL")
|
|
33
|
+
def mpds(redirect):
|
|
34
|
+
"""MPDS authentication"""
|
|
35
|
+
mpds_repo = MPDSOAuthRepository()
|
|
36
|
+
url = mpds_repo.generate_auth_url(redirect)
|
|
37
|
+
click.echo("MPDS Auth URL:")
|
|
38
|
+
click.echo(url)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@auth.command()
|
|
42
|
+
@click.argument("jwt_token")
|
|
43
|
+
def set_token(jwt_token):
|
|
44
|
+
"""Set JWT token directly"""
|
|
45
|
+
try:
|
|
46
|
+
payload = jwt.decode(jwt_token, options={"verify_signature": False})
|
|
47
|
+
user_id = int(payload.get("sub", 0))
|
|
48
|
+
token_data = {
|
|
49
|
+
"access_token": jwt_token,
|
|
50
|
+
"token_type": "bearer",
|
|
51
|
+
"user_id": user_id,
|
|
52
|
+
"expires_in": 604800,
|
|
53
|
+
"payload": payload,
|
|
54
|
+
}
|
|
55
|
+
TokenManager.save_token(token_data)
|
|
56
|
+
click.echo("Token saved")
|
|
57
|
+
click.echo(json.dumps({"user_id": user_id, "token_type": "bearer"}, indent=2))
|
|
58
|
+
except Exception as e:
|
|
59
|
+
click.echo(f"Error: {str(e)}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@auth.command()
|
|
63
|
+
def info():
|
|
64
|
+
"""Show token information"""
|
|
65
|
+
token_data = TokenManager.get_token_info()
|
|
66
|
+
if token_data:
|
|
67
|
+
click.echo("Token Information:")
|
|
68
|
+
click.echo(json.dumps(token_data, indent=2, default=str))
|
|
69
|
+
else:
|
|
70
|
+
click.echo("No token found")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@auth.command()
|
|
74
|
+
def decode():
|
|
75
|
+
"""Decode JWT token"""
|
|
76
|
+
token = TokenManager.get_token()
|
|
77
|
+
if not token:
|
|
78
|
+
click.echo("No token found")
|
|
79
|
+
return
|
|
80
|
+
try:
|
|
81
|
+
payload = jwt.decode(token, options={"verify_signature": False})
|
|
82
|
+
click.echo("JWT Payload:")
|
|
83
|
+
click.echo(json.dumps(payload, indent=2))
|
|
84
|
+
user_id = payload.get("sub")
|
|
85
|
+
if user_id:
|
|
86
|
+
click.echo(f"\nUser ID: {user_id}")
|
|
87
|
+
exp = payload.get("exp")
|
|
88
|
+
if exp:
|
|
89
|
+
exp_time = datetime.datetime.fromtimestamp(exp)
|
|
90
|
+
click.echo(f"Expires: {exp_time}")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
click.echo(f"Error decoding token: {str(e)}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@auth.command()
|
|
96
|
+
def logout():
|
|
97
|
+
"""Logout (clear token)"""
|
|
98
|
+
if TokenManager.clear_token():
|
|
99
|
+
click.echo("Logged out")
|
|
100
|
+
else:
|
|
101
|
+
click.echo("Logout failed")
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from app.src.application.services.auth_service import AuthService
|
|
7
|
+
from app.src.application.services.task_service import TaskService
|
|
8
|
+
from app.src.presentation.cli import cli
|
|
9
|
+
from app.src.presentation.utils import TokenManager, async_command, require_auth
|
|
10
|
+
|
|
11
|
+
API_URL = os.getenv("API_URL", "http://localhost:8000")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.group()
|
|
15
|
+
def task():
|
|
16
|
+
"""Task management commands"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@task.command()
|
|
21
|
+
@click.option("--page", default=1, help="Number of pages")
|
|
22
|
+
@click.option(
|
|
23
|
+
"--per_page", default=20, help="Maximum number of tasks to return per page"
|
|
24
|
+
)
|
|
25
|
+
@require_auth
|
|
26
|
+
@async_command
|
|
27
|
+
async def list(page, per_page):
|
|
28
|
+
"""List tasks"""
|
|
29
|
+
token = TokenManager.get_token()
|
|
30
|
+
auth_service = AuthService(api_url=API_URL)
|
|
31
|
+
auth_service.set_token(token)
|
|
32
|
+
task_repo = await auth_service.get_task_repository()
|
|
33
|
+
task_service = TaskService(task_repo)
|
|
34
|
+
|
|
35
|
+
tasks = await task_service.get_tasks(page=page, per_page=per_page)
|
|
36
|
+
|
|
37
|
+
click.echo(f"Found {len(tasks)} tasks")
|
|
38
|
+
for task in tasks:
|
|
39
|
+
task_id = task.get("task_id")
|
|
40
|
+
status_val = task.get("status")
|
|
41
|
+
question = task.get("question", "")[:60]
|
|
42
|
+
assignee_val = task.get("assigned_to", "Unassigned")
|
|
43
|
+
created_by = task.get("created_by")
|
|
44
|
+
click.echo(f"ID: #{task_id}")
|
|
45
|
+
click.echo(f" Status: {status_val}")
|
|
46
|
+
click.echo(f" Question: {question}")
|
|
47
|
+
click.echo(f" Assignee: {assignee_val}")
|
|
48
|
+
click.echo(f" Created by: {created_by}")
|
|
49
|
+
if "bid" in task:
|
|
50
|
+
click.echo(f" Bid: {task['bid']}")
|
|
51
|
+
click.echo()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@task.command()
|
|
55
|
+
@click.argument("task_id", type=int)
|
|
56
|
+
@require_auth
|
|
57
|
+
@async_command
|
|
58
|
+
async def get(task_id):
|
|
59
|
+
"""Get task details"""
|
|
60
|
+
token = TokenManager.get_token()
|
|
61
|
+
auth_service = AuthService(api_url=API_URL)
|
|
62
|
+
auth_service.set_token(token)
|
|
63
|
+
task_repo = await auth_service.get_task_repository()
|
|
64
|
+
task_service = TaskService(task_repo)
|
|
65
|
+
|
|
66
|
+
result = await task_service.get_task(task_id)
|
|
67
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@task.command()
|
|
71
|
+
@click.option("--question", required=True, help="Task question")
|
|
72
|
+
@click.option("--bid", type=int, required=True, help="Bid amount")
|
|
73
|
+
@click.option("--assign-to", type=int, help="Assign to user ID")
|
|
74
|
+
@click.option("--schema", type=int, help="Schema ID")
|
|
75
|
+
@click.option("--crop", type=int, help="Crop ID")
|
|
76
|
+
@require_auth
|
|
77
|
+
@async_command
|
|
78
|
+
async def create(question, bid, assign_to, schema, crop):
|
|
79
|
+
"""Create a new task"""
|
|
80
|
+
token = TokenManager.get_token()
|
|
81
|
+
auth_service = AuthService(api_url=API_URL)
|
|
82
|
+
auth_service.set_token(token)
|
|
83
|
+
task_repo = await auth_service.get_task_repository()
|
|
84
|
+
task_service = TaskService(task_repo)
|
|
85
|
+
|
|
86
|
+
task_data = {"question": question, "bid": bid}
|
|
87
|
+
if assign_to:
|
|
88
|
+
task_data["assigned_to"] = assign_to
|
|
89
|
+
task_data["assign_task"] = True
|
|
90
|
+
if schema:
|
|
91
|
+
task_data["schema_id"] = schema
|
|
92
|
+
if crop:
|
|
93
|
+
task_data["crop_id"] = crop
|
|
94
|
+
|
|
95
|
+
result = await task_service.create_task(task_data)
|
|
96
|
+
click.echo("Task created")
|
|
97
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@task.command()
|
|
101
|
+
@click.argument("task_id", type=int)
|
|
102
|
+
@click.argument("fields", nargs=-1)
|
|
103
|
+
@require_auth
|
|
104
|
+
@async_command
|
|
105
|
+
async def update(task_id, fields):
|
|
106
|
+
"""Update task fields (field=value format)"""
|
|
107
|
+
token = TokenManager.get_token()
|
|
108
|
+
auth_service = AuthService(api_url=API_URL)
|
|
109
|
+
auth_service.set_token(token)
|
|
110
|
+
task_repo = await auth_service.get_task_repository()
|
|
111
|
+
task_service = TaskService(task_repo)
|
|
112
|
+
|
|
113
|
+
update_data = {}
|
|
114
|
+
for field_pair in fields:
|
|
115
|
+
if "=" in field_pair:
|
|
116
|
+
key, value = field_pair.split("=", 1)
|
|
117
|
+
if value.lower() == "true":
|
|
118
|
+
value = True
|
|
119
|
+
elif value.lower() == "false":
|
|
120
|
+
value = False
|
|
121
|
+
elif value.isdigit():
|
|
122
|
+
value = int(value)
|
|
123
|
+
elif key == "json_data":
|
|
124
|
+
value = json.loads(value)
|
|
125
|
+
update_data[key] = value
|
|
126
|
+
|
|
127
|
+
result = await task_service.update_task(task_id, update_data)
|
|
128
|
+
click.echo("Task updated")
|
|
129
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@task.command()
|
|
133
|
+
@click.argument("task_id", type=int)
|
|
134
|
+
@require_auth
|
|
135
|
+
@async_command
|
|
136
|
+
async def delete(task_id):
|
|
137
|
+
"""Delete a task"""
|
|
138
|
+
token = TokenManager.get_token()
|
|
139
|
+
auth_service = AuthService(api_url=API_URL)
|
|
140
|
+
auth_service.set_token(token)
|
|
141
|
+
task_repo = await auth_service.get_task_repository()
|
|
142
|
+
task_service = TaskService(task_repo)
|
|
143
|
+
|
|
144
|
+
result = await task_service.delete_task(task_id)
|
|
145
|
+
click.echo("Task deleted")
|
|
146
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@task.command()
|
|
150
|
+
@click.argument("task_id", type=int)
|
|
151
|
+
@click.argument("user_id", type=int)
|
|
152
|
+
@require_auth
|
|
153
|
+
@async_command
|
|
154
|
+
async def assign(task_id, user_id):
|
|
155
|
+
"""Assign task to user"""
|
|
156
|
+
token = TokenManager.get_token()
|
|
157
|
+
auth_service = AuthService(api_url=API_URL)
|
|
158
|
+
auth_service.set_token(token)
|
|
159
|
+
task_repo = await auth_service.get_task_repository()
|
|
160
|
+
task_service = TaskService(task_repo)
|
|
161
|
+
|
|
162
|
+
result = await task_service.assign_task(task_id, user_id)
|
|
163
|
+
click.echo("Task assigned")
|
|
164
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@task.command()
|
|
168
|
+
@click.argument("task_id", type=int)
|
|
169
|
+
@require_auth
|
|
170
|
+
@async_command
|
|
171
|
+
async def start(task_id):
|
|
172
|
+
"""Start working on task"""
|
|
173
|
+
token = TokenManager.get_token()
|
|
174
|
+
auth_service = AuthService(api_url=API_URL)
|
|
175
|
+
auth_service.set_token(token)
|
|
176
|
+
task_repo = await auth_service.get_task_repository()
|
|
177
|
+
task_service = TaskService(task_repo)
|
|
178
|
+
|
|
179
|
+
result = await task_service.start_task(task_id)
|
|
180
|
+
click.echo("Task started")
|
|
181
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@task.command()
|
|
185
|
+
@click.argument("task_id", type=int)
|
|
186
|
+
@require_auth
|
|
187
|
+
@async_command
|
|
188
|
+
async def finalize(task_id):
|
|
189
|
+
"""Finalize task"""
|
|
190
|
+
token = TokenManager.get_token()
|
|
191
|
+
auth_service = AuthService(api_url=API_URL)
|
|
192
|
+
auth_service.set_token(token)
|
|
193
|
+
task_repo = await auth_service.get_task_repository()
|
|
194
|
+
task_service = TaskService(task_repo)
|
|
195
|
+
|
|
196
|
+
result = await task_service.finalize_task(task_id)
|
|
197
|
+
click.echo("Task finalized")
|
|
198
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@task.command()
|
|
202
|
+
@click.argument("task_id", type=int)
|
|
203
|
+
@require_auth
|
|
204
|
+
@async_command
|
|
205
|
+
async def approve(task_id):
|
|
206
|
+
"""Approve task"""
|
|
207
|
+
token = TokenManager.get_token()
|
|
208
|
+
auth_service = AuthService(api_url=API_URL)
|
|
209
|
+
auth_service.set_token(token)
|
|
210
|
+
task_repo = await auth_service.get_task_repository()
|
|
211
|
+
task_service = TaskService(task_repo)
|
|
212
|
+
|
|
213
|
+
result = await task_service.approve_task(task_id)
|
|
214
|
+
click.echo("Task approved")
|
|
215
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@task.command()
|
|
219
|
+
@click.argument("task_id", type=int)
|
|
220
|
+
@require_auth
|
|
221
|
+
@async_command
|
|
222
|
+
async def reject(task_id):
|
|
223
|
+
"""Reject task"""
|
|
224
|
+
token = TokenManager.get_token()
|
|
225
|
+
auth_service = AuthService(api_url=API_URL)
|
|
226
|
+
auth_service.set_token(token)
|
|
227
|
+
task_repo = await auth_service.get_task_repository()
|
|
228
|
+
task_service = TaskService(task_repo)
|
|
229
|
+
|
|
230
|
+
result = await task_service.reject_task(task_id)
|
|
231
|
+
click.echo("Task rejected")
|
|
232
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import keyring
|
|
9
|
+
|
|
10
|
+
KEYRING_SERVICE_NAME = "esdd-cli"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def async_command(f):
|
|
14
|
+
@wraps(f)
|
|
15
|
+
def wrapper(*args, **kwargs):
|
|
16
|
+
return asyncio.run(f(*args, **kwargs))
|
|
17
|
+
|
|
18
|
+
return wrapper
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TokenManager:
|
|
22
|
+
@staticmethod
|
|
23
|
+
def get_token() -> Optional[str]:
|
|
24
|
+
token_json = keyring.get_password(KEYRING_SERVICE_NAME, "access_token")
|
|
25
|
+
if token_json:
|
|
26
|
+
try:
|
|
27
|
+
token_data = json.loads(token_json)
|
|
28
|
+
return token_data.get("access_token")
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
return None
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def save_token(token_data: Dict[str, Any]) -> None:
|
|
35
|
+
token_data["created_at"] = datetime.datetime.now().isoformat()
|
|
36
|
+
token_json = json.dumps(token_data)
|
|
37
|
+
keyring.set_password(KEYRING_SERVICE_NAME, "access_token", token_json)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def clear_token() -> bool:
|
|
41
|
+
try:
|
|
42
|
+
keyring.delete_password(KEYRING_SERVICE_NAME, "access_token")
|
|
43
|
+
return True
|
|
44
|
+
except keyring.errors.PasswordDeleteError:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def get_token_info() -> Optional[Dict[str, Any]]:
|
|
49
|
+
token_json = keyring.get_password(KEYRING_SERVICE_NAME, "access_token")
|
|
50
|
+
if token_json:
|
|
51
|
+
try:
|
|
52
|
+
token_data = json.loads(token_json)
|
|
53
|
+
created_at = token_data.get("created_at")
|
|
54
|
+
expires_in = token_data.get("expires_in", 0)
|
|
55
|
+
if created_at and expires_in:
|
|
56
|
+
created = datetime.datetime.fromisoformat(created_at)
|
|
57
|
+
expires_at = created + datetime.timedelta(seconds=expires_in)
|
|
58
|
+
now = datetime.datetime.now()
|
|
59
|
+
token_data["is_valid"] = now < expires_at
|
|
60
|
+
token_data["expires_at"] = expires_at.isoformat()
|
|
61
|
+
return token_data
|
|
62
|
+
except (json.JSONDecodeError, KeyError):
|
|
63
|
+
return None
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def require_auth(f):
|
|
68
|
+
@wraps(f)
|
|
69
|
+
def wrapper(*args, **kwargs):
|
|
70
|
+
if not TokenManager.get_token():
|
|
71
|
+
click.echo("Not authenticated. Use 'esdd auth' commands first.")
|
|
72
|
+
return
|
|
73
|
+
return f(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
app/src/application/__init__.py
|
|
4
|
+
app/src/application/services/auth_service.py
|
|
5
|
+
app/src/application/services/task_service.py
|
|
6
|
+
app/src/domain/__init__.py
|
|
7
|
+
app/src/domain/repositories/auth_repository.py
|
|
8
|
+
app/src/domain/repositories/task_repository.py
|
|
9
|
+
app/src/infrastructure/__init__.py
|
|
10
|
+
app/src/infrastructure/repositories/git_oauth_repository.py
|
|
11
|
+
app/src/infrastructure/repositories/mpds_oauth_repository.py
|
|
12
|
+
app/src/infrastructure/repositories/task_repository_impl.py
|
|
13
|
+
app/src/presentation/__init__.py
|
|
14
|
+
app/src/presentation/cli.py
|
|
15
|
+
app/src/presentation/utils.py
|
|
16
|
+
app/src/presentation/routers/auth.py
|
|
17
|
+
app/src/presentation/routers/task.py
|
|
18
|
+
esdd_client.egg-info/PKG-INFO
|
|
19
|
+
esdd_client.egg-info/SOURCES.txt
|
|
20
|
+
esdd_client.egg-info/dependency_links.txt
|
|
21
|
+
esdd_client.egg-info/entry_points.txt
|
|
22
|
+
esdd_client.egg-info/requires.txt
|
|
23
|
+
esdd_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
app
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "esdd-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.25.1",
|
|
10
|
+
"click>=8.1.7",
|
|
11
|
+
"python-dotenv==1.2.1",
|
|
12
|
+
"keyring>=25.2.0"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
esdd = "app.src.presentation.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.flake8]
|
|
19
|
+
max-line-length = 100
|
|
20
|
+
|
|
21
|
+
ignore = ["E402", "F401"]
|