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.
Files changed (25) hide show
  1. esdd_client-0.1.0/PKG-INFO +7 -0
  2. esdd_client-0.1.0/README.md +79 -0
  3. esdd_client-0.1.0/app/src/application/__init__.py +0 -0
  4. esdd_client-0.1.0/app/src/application/services/auth_service.py +62 -0
  5. esdd_client-0.1.0/app/src/application/services/task_service.py +44 -0
  6. esdd_client-0.1.0/app/src/domain/__init__.py +0 -0
  7. esdd_client-0.1.0/app/src/domain/repositories/auth_repository.py +13 -0
  8. esdd_client-0.1.0/app/src/domain/repositories/task_repository.py +62 -0
  9. esdd_client-0.1.0/app/src/infrastructure/__init__.py +0 -0
  10. esdd_client-0.1.0/app/src/infrastructure/repositories/git_oauth_repository.py +35 -0
  11. esdd_client-0.1.0/app/src/infrastructure/repositories/mpds_oauth_repository.py +38 -0
  12. esdd_client-0.1.0/app/src/infrastructure/repositories/task_repository_impl.py +130 -0
  13. esdd_client-0.1.0/app/src/presentation/__init__.py +0 -0
  14. esdd_client-0.1.0/app/src/presentation/cli.py +22 -0
  15. esdd_client-0.1.0/app/src/presentation/routers/auth.py +101 -0
  16. esdd_client-0.1.0/app/src/presentation/routers/task.py +232 -0
  17. esdd_client-0.1.0/app/src/presentation/utils.py +75 -0
  18. esdd_client-0.1.0/esdd_client.egg-info/PKG-INFO +7 -0
  19. esdd_client-0.1.0/esdd_client.egg-info/SOURCES.txt +23 -0
  20. esdd_client-0.1.0/esdd_client.egg-info/dependency_links.txt +1 -0
  21. esdd_client-0.1.0/esdd_client.egg-info/entry_points.txt +2 -0
  22. esdd_client-0.1.0/esdd_client.egg-info/requires.txt +4 -0
  23. esdd_client-0.1.0/esdd_client.egg-info/top_level.txt +1 -0
  24. esdd_client-0.1.0/pyproject.toml +21 -0
  25. esdd_client-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: esdd-client
3
+ Version: 0.1.0
4
+ Requires-Dist: httpx>=0.25.1
5
+ Requires-Dist: click>=8.1.7
6
+ Requires-Dist: python-dotenv==1.2.1
7
+ Requires-Dist: keyring>=25.2.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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: esdd-client
3
+ Version: 0.1.0
4
+ Requires-Dist: httpx>=0.25.1
5
+ Requires-Dist: click>=8.1.7
6
+ Requires-Dist: python-dotenv==1.2.1
7
+ Requires-Dist: keyring>=25.2.0
@@ -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,2 @@
1
+ [console_scripts]
2
+ esdd = app.src.presentation.cli:main
@@ -0,0 +1,4 @@
1
+ httpx>=0.25.1
2
+ click>=8.1.7
3
+ python-dotenv==1.2.1
4
+ keyring>=25.2.0
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+