redtile 0.0.17__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 (58) hide show
  1. redi/__init__.py +0 -0
  2. redi/api/__init__.py +0 -0
  3. redi/api/attachment.py +110 -0
  4. redi/api/custom_field.py +72 -0
  5. redi/api/enumeration.py +44 -0
  6. redi/api/file.py +55 -0
  7. redi/api/group.py +166 -0
  8. redi/api/issue.py +367 -0
  9. redi/api/issue_category.py +123 -0
  10. redi/api/issue_relation.py +82 -0
  11. redi/api/issue_status.py +26 -0
  12. redi/api/me.py +59 -0
  13. redi/api/membership.py +113 -0
  14. redi/api/news.py +32 -0
  15. redi/api/project.py +189 -0
  16. redi/api/query.py +14 -0
  17. redi/api/role.py +45 -0
  18. redi/api/search.py +30 -0
  19. redi/api/time_entry.py +143 -0
  20. redi/api/tracker.py +26 -0
  21. redi/api/user.py +196 -0
  22. redi/api/version.py +144 -0
  23. redi/api/wiki.py +168 -0
  24. redi/cache.py +30 -0
  25. redi/cli/__init__.py +3 -0
  26. redi/cli/_common.py +214 -0
  27. redi/cli/attachment_command.py +60 -0
  28. redi/cli/config_command.py +100 -0
  29. redi/cli/enumerations_command.py +44 -0
  30. redi/cli/file_command.py +40 -0
  31. redi/cli/group_command.py +110 -0
  32. redi/cli/issue_category_command.py +104 -0
  33. redi/cli/issue_command.py +548 -0
  34. redi/cli/main.py +252 -0
  35. redi/cli/me_command.py +29 -0
  36. redi/cli/membership_command.py +115 -0
  37. redi/cli/news_command.py +15 -0
  38. redi/cli/project_command.py +153 -0
  39. redi/cli/relation_command.py +28 -0
  40. redi/cli/role_command.py +28 -0
  41. redi/cli/search_command.py +22 -0
  42. redi/cli/time_entry_command.py +99 -0
  43. redi/cli/user_command.py +148 -0
  44. redi/cli/version_command.py +289 -0
  45. redi/cli/wiki_command.py +198 -0
  46. redi/client.py +39 -0
  47. redi/config.py +206 -0
  48. redi/tui/__init__.py +24 -0
  49. redi/tui/app.py +226 -0
  50. redi/tui/issue_tab.py +173 -0
  51. redi/tui/render.py +20 -0
  52. redi/tui/state.py +54 -0
  53. redi/tui/tab.py +26 -0
  54. redi/tui/wiki_tab.py +180 -0
  55. redtile-0.0.17.dist-info/METADATA +173 -0
  56. redtile-0.0.17.dist-info/RECORD +58 -0
  57. redtile-0.0.17.dist-info/WHEEL +4 -0
  58. redtile-0.0.17.dist-info/entry_points.txt +3 -0
redi/__init__.py ADDED
File without changes
redi/api/__init__.py ADDED
File without changes
redi/api/attachment.py ADDED
@@ -0,0 +1,110 @@
1
+ import json
2
+ import mimetypes
3
+ import os
4
+
5
+ import requests
6
+
7
+ from redi.client import client
8
+ from redi.config import redmine_url
9
+
10
+
11
+ def upload_file(file_path: str) -> dict:
12
+ if not os.path.isfile(file_path):
13
+ print(f"ファイルが見つかりません: {file_path}")
14
+ exit(1)
15
+ filename = os.path.basename(file_path)
16
+ content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
17
+ with open(file_path, "rb") as f:
18
+ response = client.post(
19
+ "/uploads.json",
20
+ headers={"Content-Type": "application/octet-stream"},
21
+ data=f,
22
+ )
23
+ try:
24
+ response.raise_for_status()
25
+ except requests.exceptions.HTTPError as e:
26
+ print(e)
27
+ print(e.response.text)
28
+ print(f"ファイルのアップロードに失敗しました: {file_path}")
29
+ exit(1)
30
+ token = response.json()["upload"]["token"]
31
+ return {
32
+ "token": token,
33
+ "filename": filename,
34
+ "content_type": content_type,
35
+ }
36
+
37
+
38
+ def fetch_attachment(attachment_id: str) -> dict:
39
+ response = client.get(f"/attachments/{attachment_id}.json")
40
+ if response.status_code == 404:
41
+ print(f"添付ファイルが見つかりません: #{attachment_id}")
42
+ exit(1)
43
+ response.raise_for_status()
44
+ return response.json()["attachment"]
45
+
46
+
47
+ def read_attachment(attachment_id: str, full: bool = False) -> None:
48
+ attachment = fetch_attachment(attachment_id)
49
+ if full:
50
+ print(json.dumps(attachment, ensure_ascii=False))
51
+ return
52
+ lines = [
53
+ f"{attachment['id']} {attachment['filename']}",
54
+ f"サイズ: {attachment.get('filesize', '')}",
55
+ f"種別: {attachment.get('content_type', '')}",
56
+ ]
57
+ author = attachment.get("author") or {}
58
+ if author:
59
+ lines.append(f"作成者: {author.get('name', '')}")
60
+ if attachment.get("created_on"):
61
+ lines.append(f"作成日時: {attachment['created_on']}")
62
+ if attachment.get("description"):
63
+ lines.append(f"説明: {attachment['description']}")
64
+ if attachment.get("content_url"):
65
+ lines.append(f"URL: {attachment['content_url']}")
66
+ print("\n".join(lines))
67
+
68
+
69
+ def delete_attachment(attachment_id: str) -> None:
70
+ response = client.delete(f"/attachments/{attachment_id}.json")
71
+ if response.status_code == 404:
72
+ print(f"添付ファイルが見つかりません: #{attachment_id}")
73
+ exit(1)
74
+ try:
75
+ response.raise_for_status()
76
+ except requests.exceptions.HTTPError as e:
77
+ print(e)
78
+ print(e.response.text)
79
+ print("添付ファイルの削除に失敗しました")
80
+ exit(1)
81
+ print(f"添付ファイルを削除しました: #{attachment_id}")
82
+
83
+
84
+ def update_attachment(
85
+ attachment_id: str,
86
+ filename: str | None = None,
87
+ description: str | None = None,
88
+ ) -> None:
89
+ data: dict = {}
90
+ if filename is not None:
91
+ data["filename"] = filename
92
+ if description is not None:
93
+ data["description"] = description
94
+ if not data:
95
+ print("更新をキャンセルしました")
96
+ exit()
97
+ response = client.patch(
98
+ f"/attachments/{attachment_id}.json", json={"attachment": data}
99
+ )
100
+ if response.status_code == 404:
101
+ print(f"添付ファイルが見つかりません: #{attachment_id}")
102
+ exit(1)
103
+ try:
104
+ response.raise_for_status()
105
+ except requests.exceptions.HTTPError as e:
106
+ print(e)
107
+ print(e.response.text)
108
+ print("添付ファイルの更新に失敗しました")
109
+ exit(1)
110
+ print(f"添付ファイルを更新しました: {redmine_url}/attachments/{attachment_id}")
@@ -0,0 +1,72 @@
1
+ import json
2
+
3
+ from redi import cache
4
+ from redi.client import client
5
+
6
+ CACHE_KEY = "custom_fields"
7
+
8
+
9
+ def fetch_custom_fields() -> list[dict] | None:
10
+ cached = cache.load(CACHE_KEY)
11
+ if cached is not None:
12
+ return cached
13
+ response = client.get("/custom_fields.json")
14
+ if response.status_code == 403:
15
+ # https://www.redmine.org/projects/redmine/wiki/Rest_CustomFields
16
+ # https://www.redmine.org/issues/18875
17
+ return
18
+ response.raise_for_status()
19
+ data = response.json()["custom_fields"]
20
+ cache.save(CACHE_KEY, data)
21
+ return data
22
+
23
+
24
+ def list_custom_fields(full: bool = False) -> None:
25
+ custom_fields = fetch_custom_fields()
26
+ if custom_fields is None:
27
+ print("カスタムフィールドの取得には管理者権限が必要です")
28
+ exit(1)
29
+ if full:
30
+ print(json.dumps(custom_fields, ensure_ascii=False))
31
+ else:
32
+ for cf in custom_fields:
33
+ print(f"{cf['id']} {cf['name']}")
34
+
35
+
36
+ def fetch_project_issue_custom_field_ids(project_id: str) -> set[int]:
37
+ """プロジェクトで有効なイシュー用カスタムフィールドのIDを取得する。"""
38
+ response = client.get(
39
+ f"/projects/{project_id}.json", params={"include": "issue_custom_fields"}
40
+ )
41
+ response.raise_for_status()
42
+ project = response.json()["project"]
43
+
44
+ return {cf["id"] for cf in project.get("issue_custom_fields") or []}
45
+
46
+
47
+ def filter_required_issue_custom_fields(
48
+ custom_fields: list[dict],
49
+ project_cf_ids: set[int],
50
+ tracker_id: str | None,
51
+ ) -> list[dict]:
52
+ """
53
+ 入力必須・初期値なし・プロジェクト/トラッカーに該当する
54
+ イシュー用カスタムフィールドを抽出する。
55
+ """
56
+ result = []
57
+ for cf in custom_fields:
58
+ if cf.get("customized_type") != "issue":
59
+ continue
60
+ if not cf.get("is_required"):
61
+ continue
62
+ if cf.get("default_value"):
63
+ continue
64
+ if cf["id"] not in project_cf_ids:
65
+ continue
66
+ trackers = cf.get("trackers") or []
67
+ if trackers and tracker_id is not None:
68
+ tracker_ids = {str(t["id"]) for t in trackers}
69
+ if str(tracker_id) not in tracker_ids:
70
+ continue
71
+ result.append(cf)
72
+ return result
@@ -0,0 +1,44 @@
1
+ import json
2
+
3
+ from redi import cache
4
+ from redi.client import client
5
+
6
+
7
+ def _fetch_enumeration(resource: str) -> list[dict]:
8
+ cached = cache.load(resource)
9
+ if cached is not None:
10
+ return cached
11
+ response = client.get(f"/enumerations/{resource}.json")
12
+ response.raise_for_status()
13
+ data = response.json()[resource]
14
+ cache.save(resource, data)
15
+ return data
16
+
17
+
18
+ def _list_enumeration(resource: str, full: bool = False) -> None:
19
+ items = _fetch_enumeration(resource)
20
+ if full:
21
+ print(json.dumps(items, ensure_ascii=False))
22
+ else:
23
+ for item in items:
24
+ print(f"{item['id']} {item['name']}")
25
+
26
+
27
+ def fetch_issue_priorities() -> list[dict]:
28
+ return _fetch_enumeration("issue_priorities")
29
+
30
+
31
+ def list_issue_priorities(full: bool = False) -> None:
32
+ _list_enumeration("issue_priorities", full)
33
+
34
+
35
+ def fetch_time_entry_activities() -> list[dict]:
36
+ return _fetch_enumeration("time_entry_activities")
37
+
38
+
39
+ def list_time_entry_activities(full: bool = False) -> None:
40
+ _list_enumeration("time_entry_activities", full)
41
+
42
+
43
+ def list_document_categories(full: bool = False) -> None:
44
+ _list_enumeration("document_categories", full)
redi/api/file.py ADDED
@@ -0,0 +1,55 @@
1
+ import json
2
+
3
+ import requests
4
+
5
+ from redi.api.attachment import upload_file
6
+ from redi.client import client
7
+
8
+
9
+ def list_files(project_id: str, full: bool = False) -> None:
10
+ response = client.get(f"/projects/{project_id}/files.json")
11
+ if response.status_code == 404:
12
+ print(f"プロジェクトが見つかりません: {project_id}")
13
+ exit(1)
14
+ response.raise_for_status()
15
+ files = response.json()["files"]
16
+ if full:
17
+ print(json.dumps(files, ensure_ascii=False))
18
+ return
19
+ for f in files:
20
+ version = f.get("version") or {}
21
+ version_label = f" [{version.get('name')}]" if version else ""
22
+ size = f.get("filesize", "")
23
+ print(f"{f['id']} {f['filename']} ({size}B){version_label}")
24
+
25
+
26
+ def create_file(
27
+ project_id: str,
28
+ file_path: str,
29
+ version_id: int | None = None,
30
+ description: str | None = None,
31
+ ) -> None:
32
+ upload = upload_file(file_path)
33
+ file_data: dict = {
34
+ "token": upload["token"],
35
+ "filename": upload["filename"],
36
+ "content_type": upload["content_type"],
37
+ }
38
+ if version_id is not None:
39
+ file_data["version_id"] = version_id
40
+ if description is not None:
41
+ file_data["description"] = description
42
+ response = client.post(
43
+ f"/projects/{project_id}/files.json", json={"file": file_data}
44
+ )
45
+ if response.status_code == 404:
46
+ print(f"プロジェクトが見つかりません: {project_id}")
47
+ exit(1)
48
+ try:
49
+ response.raise_for_status()
50
+ except requests.exceptions.HTTPError as e:
51
+ print(e)
52
+ print(e.response.text)
53
+ print("ファイルのアップロードに失敗しました")
54
+ exit(1)
55
+ print(f"ファイルをアップロードしました: {upload['filename']}")
redi/api/group.py ADDED
@@ -0,0 +1,166 @@
1
+ import json
2
+
3
+ import requests
4
+
5
+ from redi.client import client
6
+ from redi.config import redmine_url
7
+
8
+
9
+ def list_groups(full: bool = False) -> None:
10
+ response = client.get("/groups.json")
11
+ response.raise_for_status()
12
+ groups = response.json()["groups"]
13
+ if full:
14
+ print(json.dumps(groups, ensure_ascii=False))
15
+ else:
16
+ for group in groups:
17
+ print(f"{group['id']} {group['name']}")
18
+
19
+
20
+ def fetch_group(group_id: str, include: str = "") -> dict:
21
+ params: dict = {}
22
+ if include:
23
+ params["include"] = include
24
+ response = client.get(f"/groups/{group_id}.json", params=params)
25
+ if response.status_code == 404:
26
+ print(f"グループが見つかりません: #{group_id}")
27
+ exit(1)
28
+ if response.status_code == 403:
29
+ print("グループの取得には管理者権限が必要です")
30
+ exit(1)
31
+ response.raise_for_status()
32
+ return response.json()["group"]
33
+
34
+
35
+ def read_group(group_id: str, full: bool = False) -> None:
36
+ group = fetch_group(group_id, include="users,memberships")
37
+ if full:
38
+ print(json.dumps(group, ensure_ascii=False))
39
+ return
40
+ lines = [f"{group['id']} {group['name']}"]
41
+ users = group.get("users") or []
42
+ if users:
43
+ lines.append("")
44
+ lines.append("ユーザー:")
45
+ for u in users:
46
+ lines.append(f" {u['id']} {u['name']}")
47
+ memberships = group.get("memberships") or []
48
+ if memberships:
49
+ lines.append("")
50
+ lines.append("メンバーシップ:")
51
+ for m in memberships:
52
+ project = m.get("project") or {}
53
+ roles = m.get("roles") or []
54
+ role_names = ", ".join(r.get("name", "") for r in roles)
55
+ lines.append(
56
+ f" {project.get('id')} {project.get('name', '')} [{role_names}]"
57
+ )
58
+ print("\n".join(lines))
59
+
60
+
61
+ def update_group(
62
+ group_id: str,
63
+ name: str | None = None,
64
+ user_ids: list[int] | None = None,
65
+ ) -> None:
66
+ data: dict = {}
67
+ if name is not None:
68
+ data["name"] = name
69
+ if user_ids is not None:
70
+ data["user_ids"] = user_ids
71
+ if len(data) == 0:
72
+ print("更新をキャンセルしました")
73
+ exit()
74
+ response = client.put(f"/groups/{group_id}.json", json={"group": data})
75
+ if response.status_code == 404:
76
+ print(f"グループが見つかりません: #{group_id}")
77
+ exit(1)
78
+ if response.status_code == 403:
79
+ print("グループの更新には管理者権限が必要です")
80
+ exit(1)
81
+ try:
82
+ response.raise_for_status()
83
+ except requests.exceptions.HTTPError as e:
84
+ print(e)
85
+ print(e.response.text)
86
+ print("グループの更新に失敗しました")
87
+ exit(1)
88
+ print(f"グループを更新しました: {group_id}")
89
+
90
+
91
+ def add_group_user(group_id: str, user_id: int) -> None:
92
+ response = client.post(
93
+ f"/groups/{group_id}/users.json",
94
+ json={"user_id": user_id},
95
+ )
96
+ if response.status_code == 404:
97
+ print(f"グループが見つかりません: #{group_id}")
98
+ exit(1)
99
+ if response.status_code == 403:
100
+ print("グループへのユーザー追加には管理者権限が必要です")
101
+ exit(1)
102
+ try:
103
+ response.raise_for_status()
104
+ except requests.exceptions.HTTPError as e:
105
+ print(e)
106
+ print(e.response.text)
107
+ print("グループへのユーザー追加に失敗しました")
108
+ exit(1)
109
+ print(f"グループ {group_id} にユーザー {user_id} を追加しました")
110
+
111
+
112
+ def remove_group_user(group_id: str, user_id: int) -> None:
113
+ response = client.delete(f"/groups/{group_id}/users/{user_id}.json")
114
+ if response.status_code == 404:
115
+ print(f"グループまたはユーザーが見つかりません: #{group_id} / #{user_id}")
116
+ exit(1)
117
+ if response.status_code == 403:
118
+ print("グループからのユーザー削除には管理者権限が必要です")
119
+ exit(1)
120
+ try:
121
+ response.raise_for_status()
122
+ except requests.exceptions.HTTPError as e:
123
+ print(e)
124
+ print(e.response.text)
125
+ print("グループからのユーザー削除に失敗しました")
126
+ exit(1)
127
+ print(f"グループ {group_id} からユーザー {user_id} を削除しました")
128
+
129
+
130
+ def delete_group(group_id: str) -> None:
131
+ response = client.delete(f"/groups/{group_id}.json")
132
+ if response.status_code == 404:
133
+ print(f"グループが見つかりません: #{group_id}")
134
+ exit(1)
135
+ if response.status_code == 403:
136
+ print("グループの削除には管理者権限が必要です")
137
+ exit(1)
138
+ try:
139
+ response.raise_for_status()
140
+ except requests.exceptions.HTTPError as e:
141
+ print(e)
142
+ print(e.response.text)
143
+ print("グループの削除に失敗しました")
144
+ exit(1)
145
+ print(f"グループを削除しました: {group_id}")
146
+
147
+
148
+ def create_group(name: str, user_ids: list[int] | None = None) -> None:
149
+ group_data: dict = {"name": name}
150
+ if user_ids:
151
+ group_data["user_ids"] = user_ids
152
+ response = client.post("/groups.json", json={"group": group_data})
153
+ if response.status_code == 403:
154
+ print("グループの作成には管理者権限が必要です")
155
+ exit(1)
156
+ try:
157
+ response.raise_for_status()
158
+ except requests.exceptions.HTTPError as e:
159
+ print(e)
160
+ print(e.response.text)
161
+ print("グループの作成に失敗しました")
162
+ exit(1)
163
+ created = response.json()["group"]
164
+ print(
165
+ f"グループを作成しました: {created['id']} {created['name']} {redmine_url}/groups/{created['id']}"
166
+ )