issuedb 1.0.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.
issuedb/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """IssueDB - A command-line issue tracking system for software development projects."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Your Name"
5
+ __email__ = "your.email@example.com"
6
+
7
+ from issuedb.models import Issue, Priority, Status
8
+
9
+ __all__ = ["Issue", "Priority", "Status"]
issuedb/cli.py ADDED
@@ -0,0 +1,520 @@
1
+ """Command-line interface for IssueDB."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from typing import Any, Optional
7
+
8
+ from issuedb.models import Issue, Priority, Status
9
+ from issuedb.repository import IssueRepository
10
+
11
+
12
+ class CLI:
13
+ """Command-line interface handler."""
14
+
15
+ def __init__(self, db_path: Optional[str] = None) -> None:
16
+ """Initialize CLI with repository.
17
+
18
+ Args:
19
+ db_path: Optional path to database file.
20
+ """
21
+ self.repo = IssueRepository(db_path)
22
+
23
+ def format_output(self, data: Any, as_json: bool = False) -> str:
24
+ """Format output for display.
25
+
26
+ Args:
27
+ data: Data to format (Issue, list of Issues, dict, etc).
28
+ as_json: If True, output as JSON.
29
+
30
+ Returns:
31
+ Formatted string output.
32
+ """
33
+ if as_json:
34
+ if isinstance(data, Issue):
35
+ return json.dumps(data.to_dict(), indent=2)
36
+ elif isinstance(data, list) and all(isinstance(i, Issue) for i in data):
37
+ return json.dumps([i.to_dict() for i in data], indent=2)
38
+ elif isinstance(data, dict):
39
+ return json.dumps(data, indent=2)
40
+ else:
41
+ return json.dumps(data, indent=2)
42
+ else:
43
+ if isinstance(data, Issue):
44
+ return self._format_issue(data)
45
+ elif isinstance(data, list) and all(isinstance(i, Issue) for i in data):
46
+ if not data:
47
+ return "No issues found."
48
+ return "\n\n".join(self._format_issue(i) for i in data)
49
+ elif isinstance(data, dict):
50
+ return self._format_dict(data)
51
+ else:
52
+ return str(data)
53
+
54
+ def _format_issue(self, issue: Issue) -> str:
55
+ """Format a single issue for display.
56
+
57
+ Args:
58
+ issue: Issue to format.
59
+
60
+ Returns:
61
+ Formatted string.
62
+ """
63
+ lines = [
64
+ f"ID: {issue.id}",
65
+ f"Title: {issue.title}",
66
+ f"Project: {issue.project}",
67
+ f"Status: {issue.status.value}",
68
+ f"Priority: {issue.priority.value}",
69
+ ]
70
+
71
+ if issue.description:
72
+ lines.append(f"Description: {issue.description}")
73
+
74
+ lines.extend(
75
+ [
76
+ f"Created: {issue.created_at.strftime('%Y-%m-%d %H:%M:%S')}",
77
+ f"Updated: {issue.updated_at.strftime('%Y-%m-%d %H:%M:%S')}",
78
+ ]
79
+ )
80
+
81
+ return "\n".join(lines)
82
+
83
+ def _format_dict(self, data: dict) -> str:
84
+ """Format a dictionary for display.
85
+
86
+ Args:
87
+ data: Dictionary to format.
88
+
89
+ Returns:
90
+ Formatted string.
91
+ """
92
+ lines = []
93
+ for key, value in data.items():
94
+ formatted_key = key.replace("_", " ").title()
95
+ lines.append(f"{formatted_key}: {value}")
96
+ return "\n".join(lines)
97
+
98
+ def create_issue(
99
+ self,
100
+ title: str,
101
+ project: str,
102
+ description: Optional[str] = None,
103
+ priority: str = "medium",
104
+ status: str = "open",
105
+ as_json: bool = False,
106
+ ) -> str:
107
+ """Create a new issue.
108
+
109
+ Args:
110
+ title: Issue title.
111
+ project: Project name.
112
+ description: Optional description.
113
+ priority: Priority level.
114
+ status: Initial status.
115
+ as_json: Output as JSON.
116
+
117
+ Returns:
118
+ Formatted output.
119
+ """
120
+ issue = Issue(
121
+ title=title,
122
+ project=project,
123
+ description=description,
124
+ priority=Priority.from_string(priority),
125
+ status=Status.from_string(status),
126
+ )
127
+
128
+ created_issue = self.repo.create_issue(issue)
129
+ return self.format_output(created_issue, as_json)
130
+
131
+ def list_issues(
132
+ self,
133
+ project: Optional[str] = None,
134
+ status: Optional[str] = None,
135
+ priority: Optional[str] = None,
136
+ limit: Optional[int] = None,
137
+ as_json: bool = False,
138
+ ) -> str:
139
+ """List issues with filters.
140
+
141
+ Args:
142
+ project: Filter by project.
143
+ status: Filter by status.
144
+ priority: Filter by priority.
145
+ limit: Maximum number of issues.
146
+ as_json: Output as JSON.
147
+
148
+ Returns:
149
+ Formatted output.
150
+ """
151
+ issues = self.repo.list_issues(
152
+ project=project, status=status, priority=priority, limit=limit
153
+ )
154
+ return self.format_output(issues, as_json)
155
+
156
+ def get_issue(self, issue_id: int, as_json: bool = False) -> str:
157
+ """Get a specific issue.
158
+
159
+ Args:
160
+ issue_id: Issue ID.
161
+ as_json: Output as JSON.
162
+
163
+ Returns:
164
+ Formatted output.
165
+
166
+ Raises:
167
+ ValueError: If issue not found.
168
+ """
169
+ issue = self.repo.get_issue(issue_id)
170
+ if not issue:
171
+ raise ValueError(f"Issue {issue_id} not found")
172
+ return self.format_output(issue, as_json)
173
+
174
+ def update_issue(self, issue_id: int, as_json: bool = False, **updates) -> str:
175
+ """Update an issue.
176
+
177
+ Args:
178
+ issue_id: Issue ID.
179
+ as_json: Output as JSON.
180
+ **updates: Fields to update.
181
+
182
+ Returns:
183
+ Formatted output.
184
+
185
+ Raises:
186
+ ValueError: If issue not found.
187
+ """
188
+ issue = self.repo.update_issue(issue_id, **updates)
189
+ if not issue:
190
+ raise ValueError(f"Issue {issue_id} not found")
191
+ return self.format_output(issue, as_json)
192
+
193
+ def delete_issue(self, issue_id: int, as_json: bool = False) -> str:
194
+ """Delete an issue.
195
+
196
+ Args:
197
+ issue_id: Issue ID.
198
+ as_json: Output as JSON.
199
+
200
+ Returns:
201
+ Formatted output.
202
+
203
+ Raises:
204
+ ValueError: If issue not found.
205
+ """
206
+ if not self.repo.delete_issue(issue_id):
207
+ raise ValueError(f"Issue {issue_id} not found")
208
+
209
+ result = {"message": f"Issue {issue_id} deleted successfully"}
210
+ return self.format_output(result, as_json)
211
+
212
+ def get_next_issue(
213
+ self, project: Optional[str] = None, status: Optional[str] = None, as_json: bool = False
214
+ ) -> str:
215
+ """Get next issue to work on.
216
+
217
+ Args:
218
+ project: Filter by project.
219
+ status: Filter by status.
220
+ as_json: Output as JSON.
221
+
222
+ Returns:
223
+ Formatted output.
224
+ """
225
+ issue = self.repo.get_next_issue(project=project, status=status)
226
+ if not issue:
227
+ result = {"message": "No issues found matching criteria"}
228
+ return self.format_output(result, as_json)
229
+ return self.format_output(issue, as_json)
230
+
231
+ def search_issues(
232
+ self,
233
+ keyword: str,
234
+ project: Optional[str] = None,
235
+ limit: Optional[int] = None,
236
+ as_json: bool = False,
237
+ ) -> str:
238
+ """Search issues by keyword.
239
+
240
+ Args:
241
+ keyword: Search keyword.
242
+ project: Filter by project.
243
+ limit: Maximum results.
244
+ as_json: Output as JSON.
245
+
246
+ Returns:
247
+ Formatted output.
248
+ """
249
+ issues = self.repo.search_issues(keyword=keyword, project=project, limit=limit)
250
+ return self.format_output(issues, as_json)
251
+
252
+ def clear_project(self, project: str, confirm: bool = False, as_json: bool = False) -> str:
253
+ """Clear all issues for a project.
254
+
255
+ Args:
256
+ project: Project name.
257
+ confirm: Safety confirmation.
258
+ as_json: Output as JSON.
259
+
260
+ Returns:
261
+ Formatted output.
262
+
263
+ Raises:
264
+ ValueError: If not confirmed.
265
+ """
266
+ if not confirm:
267
+ raise ValueError("Must use --confirm flag to clear project")
268
+
269
+ count = self.repo.clear_project(project)
270
+ result = {"message": f"Cleared {count} issues from project {project}"}
271
+ return self.format_output(result, as_json)
272
+
273
+ def get_audit_logs(
274
+ self,
275
+ issue_id: Optional[int] = None,
276
+ project: Optional[str] = None,
277
+ as_json: bool = False,
278
+ ) -> str:
279
+ """Get audit logs.
280
+
281
+ Args:
282
+ issue_id: Filter by issue ID.
283
+ project: Filter by project.
284
+ as_json: Output as JSON.
285
+
286
+ Returns:
287
+ Formatted output.
288
+ """
289
+ logs = self.repo.get_audit_logs(issue_id=issue_id, project=project)
290
+
291
+ if as_json:
292
+ return json.dumps([log.to_dict() for log in logs], indent=2)
293
+ else:
294
+ if not logs:
295
+ return "No audit logs found."
296
+
297
+ lines = []
298
+ for log in logs:
299
+ lines.append("-" * 50)
300
+ lines.append(f"Timestamp: {log.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
301
+ lines.append(f"Issue ID: {log.issue_id}")
302
+ lines.append(f"Project: {log.project}")
303
+ lines.append(f"Action: {log.action}")
304
+
305
+ if log.field_name:
306
+ lines.append(f"Field: {log.field_name}")
307
+ lines.append(f"Old Value: {log.old_value}")
308
+ lines.append(f"New Value: {log.new_value}")
309
+ elif log.action == "CREATE":
310
+ lines.append(f"Created: {log.new_value}")
311
+ elif log.action == "DELETE":
312
+ lines.append(f"Deleted: {log.old_value}")
313
+
314
+ return "\n".join(lines)
315
+
316
+ def get_info(self, as_json: bool = False) -> str:
317
+ """Get database information.
318
+
319
+ Args:
320
+ as_json: Output as JSON.
321
+
322
+ Returns:
323
+ Formatted output.
324
+ """
325
+ info = self.repo.db.get_database_info()
326
+ return self.format_output(info, as_json)
327
+
328
+
329
+ def main() -> None:
330
+ """Main entry point for the CLI."""
331
+ parser = argparse.ArgumentParser(
332
+ prog="issuedb-cli",
333
+ description="Command-line issue tracking system for software development projects",
334
+ )
335
+
336
+ parser.add_argument(
337
+ "--db",
338
+ help="Path to database file (default: ~/.issuedb/issuedb.sqlite)",
339
+ default=None,
340
+ )
341
+
342
+ parser.add_argument(
343
+ "--json",
344
+ action="store_true",
345
+ help="Output results in JSON format",
346
+ )
347
+
348
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
349
+
350
+ # Create command
351
+ create_parser = subparsers.add_parser("create", help="Create a new issue")
352
+ create_parser.add_argument("-t", "--title", required=True, help="Issue title")
353
+ create_parser.add_argument("-p", "--project", required=True, help="Project name")
354
+ create_parser.add_argument("-d", "--description", help="Issue description")
355
+ create_parser.add_argument(
356
+ "--priority",
357
+ choices=["low", "medium", "high", "critical"],
358
+ default="medium",
359
+ help="Priority level",
360
+ )
361
+ create_parser.add_argument(
362
+ "--status",
363
+ choices=["open", "in-progress", "closed"],
364
+ default="open",
365
+ help="Initial status",
366
+ )
367
+
368
+ # List command
369
+ list_parser = subparsers.add_parser("list", help="List issues")
370
+ list_parser.add_argument("-p", "--project", help="Filter by project")
371
+ list_parser.add_argument("-s", "--status", help="Filter by status (open, in-progress, closed)")
372
+ list_parser.add_argument("--priority", help="Filter by priority (low, medium, high, critical)")
373
+ list_parser.add_argument("-l", "--limit", type=int, help="Maximum number of issues")
374
+
375
+ # Get command
376
+ get_parser = subparsers.add_parser("get", help="Get issue details")
377
+ get_parser.add_argument("id", type=int, help="Issue ID")
378
+
379
+ # Update command
380
+ update_parser = subparsers.add_parser("update", help="Update an issue")
381
+ update_parser.add_argument("id", type=int, help="Issue ID")
382
+ update_parser.add_argument("-t", "--title", help="New title")
383
+ update_parser.add_argument("-p", "--project", help="New project")
384
+ update_parser.add_argument("-d", "--description", help="New description")
385
+ update_parser.add_argument(
386
+ "--priority",
387
+ choices=["low", "medium", "high", "critical"],
388
+ help="New priority",
389
+ )
390
+ update_parser.add_argument(
391
+ "-s",
392
+ "--status",
393
+ choices=["open", "in-progress", "closed"],
394
+ help="New status",
395
+ )
396
+
397
+ # Delete command
398
+ delete_parser = subparsers.add_parser("delete", help="Delete an issue")
399
+ delete_parser.add_argument("id", type=int, help="Issue ID")
400
+
401
+ # Get-next command
402
+ next_parser = subparsers.add_parser(
403
+ "get-next", help="Get next issue to work on (FIFO by priority)"
404
+ )
405
+ next_parser.add_argument("-p", "--project", help="Filter by project")
406
+ next_parser.add_argument("-s", "--status", help="Filter by status (defaults to 'open')")
407
+
408
+ # Search command
409
+ search_parser = subparsers.add_parser("search", help="Search issues by keyword")
410
+ search_parser.add_argument("-k", "--keyword", required=True, help="Search keyword")
411
+ search_parser.add_argument("-p", "--project", help="Filter by project")
412
+ search_parser.add_argument("-l", "--limit", type=int, help="Maximum results")
413
+
414
+ # Clear command
415
+ clear_parser = subparsers.add_parser("clear", help="Clear all issues for a project")
416
+ clear_parser.add_argument("-p", "--project", required=True, help="Project name")
417
+ clear_parser.add_argument("--confirm", action="store_true", help="Confirm deletion (required)")
418
+
419
+ # Audit command
420
+ audit_parser = subparsers.add_parser("audit", help="View audit logs")
421
+ audit_parser.add_argument("-i", "--issue", type=int, help="Filter by issue ID")
422
+ audit_parser.add_argument("-p", "--project", help="Filter by project")
423
+
424
+ # Info command
425
+ subparsers.add_parser("info", help="Get database information")
426
+
427
+ args = parser.parse_args()
428
+
429
+ if not args.command:
430
+ parser.print_help()
431
+ sys.exit(1)
432
+
433
+ try:
434
+ cli = CLI(args.db)
435
+
436
+ if args.command == "create":
437
+ result = cli.create_issue(
438
+ title=args.title,
439
+ project=args.project,
440
+ description=args.description,
441
+ priority=args.priority,
442
+ status=args.status,
443
+ as_json=args.json,
444
+ )
445
+ print(result)
446
+
447
+ elif args.command == "list":
448
+ result = cli.list_issues(
449
+ project=args.project,
450
+ status=args.status,
451
+ priority=args.priority,
452
+ limit=args.limit,
453
+ as_json=args.json,
454
+ )
455
+ print(result)
456
+
457
+ elif args.command == "get":
458
+ result = cli.get_issue(args.id, as_json=args.json)
459
+ print(result)
460
+
461
+ elif args.command == "update":
462
+ updates = {}
463
+ if args.title:
464
+ updates["title"] = args.title
465
+ if args.project:
466
+ updates["project"] = args.project
467
+ if args.description:
468
+ updates["description"] = args.description
469
+ if args.priority:
470
+ updates["priority"] = args.priority
471
+ if args.status:
472
+ updates["status"] = args.status
473
+
474
+ if not updates:
475
+ print("Error: No updates specified", file=sys.stderr)
476
+ sys.exit(1)
477
+
478
+ result = cli.update_issue(args.id, as_json=args.json, **updates)
479
+ print(result)
480
+
481
+ elif args.command == "delete":
482
+ result = cli.delete_issue(args.id, as_json=args.json)
483
+ print(result)
484
+
485
+ elif args.command == "get-next":
486
+ result = cli.get_next_issue(project=args.project, status=args.status, as_json=args.json)
487
+ print(result)
488
+
489
+ elif args.command == "search":
490
+ result = cli.search_issues(
491
+ keyword=args.keyword,
492
+ project=args.project,
493
+ limit=args.limit,
494
+ as_json=args.json,
495
+ )
496
+ print(result)
497
+
498
+ elif args.command == "clear":
499
+ result = cli.clear_project(
500
+ project=args.project, confirm=args.confirm, as_json=args.json
501
+ )
502
+ print(result)
503
+
504
+ elif args.command == "audit":
505
+ result = cli.get_audit_logs(
506
+ issue_id=args.issue, project=args.project, as_json=args.json
507
+ )
508
+ print(result)
509
+
510
+ elif args.command == "info":
511
+ result = cli.get_info(as_json=args.json)
512
+ print(result)
513
+
514
+ except Exception as e:
515
+ print(f"Error: {e}", file=sys.stderr)
516
+ sys.exit(1)
517
+
518
+
519
+ if __name__ == "__main__":
520
+ main()