redtile 0.0.17__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 (57) hide show
  1. redtile-0.0.17/PKG-INFO +173 -0
  2. redtile-0.0.17/README.md +159 -0
  3. redtile-0.0.17/pyproject.toml +33 -0
  4. redtile-0.0.17/src/redi/__init__.py +0 -0
  5. redtile-0.0.17/src/redi/api/__init__.py +0 -0
  6. redtile-0.0.17/src/redi/api/attachment.py +110 -0
  7. redtile-0.0.17/src/redi/api/custom_field.py +72 -0
  8. redtile-0.0.17/src/redi/api/enumeration.py +44 -0
  9. redtile-0.0.17/src/redi/api/file.py +55 -0
  10. redtile-0.0.17/src/redi/api/group.py +166 -0
  11. redtile-0.0.17/src/redi/api/issue.py +367 -0
  12. redtile-0.0.17/src/redi/api/issue_category.py +123 -0
  13. redtile-0.0.17/src/redi/api/issue_relation.py +82 -0
  14. redtile-0.0.17/src/redi/api/issue_status.py +26 -0
  15. redtile-0.0.17/src/redi/api/me.py +59 -0
  16. redtile-0.0.17/src/redi/api/membership.py +113 -0
  17. redtile-0.0.17/src/redi/api/news.py +32 -0
  18. redtile-0.0.17/src/redi/api/project.py +189 -0
  19. redtile-0.0.17/src/redi/api/query.py +14 -0
  20. redtile-0.0.17/src/redi/api/role.py +45 -0
  21. redtile-0.0.17/src/redi/api/search.py +30 -0
  22. redtile-0.0.17/src/redi/api/time_entry.py +143 -0
  23. redtile-0.0.17/src/redi/api/tracker.py +26 -0
  24. redtile-0.0.17/src/redi/api/user.py +196 -0
  25. redtile-0.0.17/src/redi/api/version.py +144 -0
  26. redtile-0.0.17/src/redi/api/wiki.py +168 -0
  27. redtile-0.0.17/src/redi/cache.py +30 -0
  28. redtile-0.0.17/src/redi/cli/__init__.py +3 -0
  29. redtile-0.0.17/src/redi/cli/_common.py +214 -0
  30. redtile-0.0.17/src/redi/cli/attachment_command.py +60 -0
  31. redtile-0.0.17/src/redi/cli/config_command.py +100 -0
  32. redtile-0.0.17/src/redi/cli/enumerations_command.py +44 -0
  33. redtile-0.0.17/src/redi/cli/file_command.py +40 -0
  34. redtile-0.0.17/src/redi/cli/group_command.py +110 -0
  35. redtile-0.0.17/src/redi/cli/issue_category_command.py +104 -0
  36. redtile-0.0.17/src/redi/cli/issue_command.py +548 -0
  37. redtile-0.0.17/src/redi/cli/main.py +252 -0
  38. redtile-0.0.17/src/redi/cli/me_command.py +29 -0
  39. redtile-0.0.17/src/redi/cli/membership_command.py +115 -0
  40. redtile-0.0.17/src/redi/cli/news_command.py +15 -0
  41. redtile-0.0.17/src/redi/cli/project_command.py +153 -0
  42. redtile-0.0.17/src/redi/cli/relation_command.py +28 -0
  43. redtile-0.0.17/src/redi/cli/role_command.py +28 -0
  44. redtile-0.0.17/src/redi/cli/search_command.py +22 -0
  45. redtile-0.0.17/src/redi/cli/time_entry_command.py +99 -0
  46. redtile-0.0.17/src/redi/cli/user_command.py +148 -0
  47. redtile-0.0.17/src/redi/cli/version_command.py +289 -0
  48. redtile-0.0.17/src/redi/cli/wiki_command.py +198 -0
  49. redtile-0.0.17/src/redi/client.py +39 -0
  50. redtile-0.0.17/src/redi/config.py +206 -0
  51. redtile-0.0.17/src/redi/tui/__init__.py +24 -0
  52. redtile-0.0.17/src/redi/tui/app.py +226 -0
  53. redtile-0.0.17/src/redi/tui/issue_tab.py +173 -0
  54. redtile-0.0.17/src/redi/tui/render.py +20 -0
  55. redtile-0.0.17/src/redi/tui/state.py +54 -0
  56. redtile-0.0.17/src/redi/tui/tab.py +26 -0
  57. redtile-0.0.17/src/redi/tui/wiki_tab.py +180 -0
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.3
2
+ Name: redtile
3
+ Version: 0.0.17
4
+ Summary: Redmine CLI tool
5
+ Author: kawagh
6
+ Author-email: kawagh <kawagh.dev@gmail.com>
7
+ Requires-Dist: argcomplete>=3.6.3
8
+ Requires-Dist: prompt-toolkit>=3.0.52
9
+ Requires-Dist: requests>=2.33.1
10
+ Requires-Dist: tomlkit>=0.14.0
11
+ Requires-Dist: wcwidth>=0.2.13
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # redi
16
+
17
+ redmine CLI tool
18
+
19
+ ## install
20
+
21
+ Install via [uv](https://github.com/astral-sh/uv)
22
+
23
+ ```shell
24
+ uv tool install https://github.com/kawagh/redi.git
25
+ ```
26
+
27
+ ## install(for development)
28
+
29
+ In repository root
30
+
31
+ ```sh
32
+ uv tool install -e .
33
+ ```
34
+
35
+ ## setup
36
+
37
+ ### config
38
+
39
+ To use redi, you need to set remdine url and redmine_api_key in one of below ways.
40
+
41
+ #### environment variable
42
+
43
+ ```sh
44
+ export REDMINE_URL=xxx
45
+ export REDMINE_API_KEY=yyy
46
+ ```
47
+
48
+ #### ~/.config/redi/config.toml
49
+
50
+ ```toml
51
+ default_profile = "main"
52
+
53
+ ["main"]
54
+ redmine_url = "xxx"
55
+ redmine_api_key = "yyy"
56
+ default_project_id = "1"
57
+ wiki_project_id = "2"
58
+ editor = "nvim"
59
+
60
+ ["sub"]
61
+ redmine_url = "vvv"
62
+ redmine_api_key = "www"
63
+ default_project_id = "2"
64
+ wiki_project_id = "3"
65
+ editor = "code"
66
+ ```
67
+
68
+ ### setup completion
69
+
70
+ ```sh
71
+ uv tool install argcomplete
72
+ echo 'eval "$(register-python-argcomplete redi)"' >> ~/.zshrc
73
+ ```
74
+
75
+ ## usage(example)
76
+
77
+ ```sh
78
+ # config (alias: c)
79
+ redi config
80
+ redi config create <profile_name> --url <url> --api_key <key> # create new profile
81
+ redi config create <profile_name> --url <url> --api_key <key> --set_default
82
+ redi config update --default_profile <profile_name> # switch profile
83
+ redi config update <profile_name> --editor nvim # update profile
84
+ redi --profile <profile_name> issue # 一時的にプロファイルを切り替えて実行
85
+
86
+ # project (alias: p)
87
+ redi project # list projects
88
+ redi project list # 同上 (`redi project l` / `redi p list` / `redi p l` / `redi p` も同じ)
89
+ redi project view <project_id> # view project
90
+ redi project view <project_id> --include trackers,issue_categories
91
+ redi project create <name> <identifier>
92
+ redi project create <name> <identifier> -d "description" --is_public true
93
+ redi project update <project_id> --name renamed_project
94
+
95
+ # issue (alias: i)
96
+ redi issue # list issues
97
+ redi issue -p <project_id> -a me -s open
98
+ redi issue -q <query_id>
99
+ redi issue view <issue_id>
100
+ redi issue view <issue_id> --web # view issue with web browser
101
+ redi issue view <issue_id> --include journals,attachments,relations
102
+ redi issue create # (interactive)
103
+ redi issue create "subject" -p <project_id> -t <tracker_id> -a <user_id> -d "description"
104
+ redi issue update <issue_id> # (interactive)
105
+ redi issue update <issue_id> --status_id <status_id> -n "notes"
106
+ redi issue update <issue_id> --relate relates --to <other_issue_id>
107
+ redi issue update <issue_id> --attach ./foo.png --attach ./bar.log
108
+ redi issue comment <issue_id> "hello~"
109
+ redi issue delete <issue_id> # (confirm before delete)
110
+ redi issue delete <issue_id> -y # skip confirmation
111
+
112
+ # version (alias: v)
113
+ redi version # list versions(fixed_versions)
114
+ redi version -p <project_id>
115
+ redi version view <version_id>
116
+ redi version create <name> -p <project_id> --due_date 2026-12-31 --status open
117
+ redi version update <version_id> --status closed
118
+
119
+ # wiki (alias: w)
120
+ redi wiki
121
+ redi wiki -p <project_id>
122
+ redi wiki view <page_title>
123
+ redi wiki create # (interactive)
124
+ redi wiki update # (interactive)
125
+
126
+ # file (プロジェクトファイル)
127
+ redi file -p <project_id> # list
128
+ redi file create ./foo.zip -p <project_id> -d "description"
129
+
130
+ # attachment
131
+ redi attachment view <attachment_id>
132
+ redi attachment update <attachment_id> -f new_name.png -d "desc"
133
+ redi attachment delete <attachment_id> # confirm before delete (-y to skip)
134
+
135
+ # relation (イシュー関係性詳細)
136
+ redi relation view <relation_id>
137
+
138
+ # time_entry (作業時間)
139
+ redi time_entry -p <project_id> -u me
140
+ redi time_entry create 1.5 -i <issue_id> -a <activity_id> -c "comment"
141
+ redi time_entry update <time_entry_id> --hours 2.0
142
+ redi time_entry delete <time_entry_id> # confirm before delete (-y to skip)
143
+
144
+ # me (自分のアカウント)
145
+ redi me
146
+ redi me update -f <firstname> -l <lastname> -m <mail>
147
+
148
+ # membership (alias: m)
149
+ redi membership -p <project_id>
150
+ redi membership view <membership_id>
151
+
152
+ # news
153
+ redi news -p <project_id>
154
+
155
+ # issue_category
156
+ redi issue_category -p <project_id>
157
+ redi issue_category create "category" -p <project_id>
158
+
159
+ # others
160
+ redi user # list users (alias: u)
161
+ redi tracker # list trackers
162
+ redi issue_status # list issue statuses
163
+ redi issue_priority # list priorities
164
+ redi time_entry_activity # list activities
165
+ redi document_category # list document categories
166
+ redi role # list roles
167
+ redi group # list groups
168
+ redi custom_field # list custom fields
169
+ redi query # list custom queries
170
+ redi search "keyword"
171
+ redi --version
172
+ redi --tui
173
+ ```
@@ -0,0 +1,159 @@
1
+ # redi
2
+
3
+ redmine CLI tool
4
+
5
+ ## install
6
+
7
+ Install via [uv](https://github.com/astral-sh/uv)
8
+
9
+ ```shell
10
+ uv tool install https://github.com/kawagh/redi.git
11
+ ```
12
+
13
+ ## install(for development)
14
+
15
+ In repository root
16
+
17
+ ```sh
18
+ uv tool install -e .
19
+ ```
20
+
21
+ ## setup
22
+
23
+ ### config
24
+
25
+ To use redi, you need to set remdine url and redmine_api_key in one of below ways.
26
+
27
+ #### environment variable
28
+
29
+ ```sh
30
+ export REDMINE_URL=xxx
31
+ export REDMINE_API_KEY=yyy
32
+ ```
33
+
34
+ #### ~/.config/redi/config.toml
35
+
36
+ ```toml
37
+ default_profile = "main"
38
+
39
+ ["main"]
40
+ redmine_url = "xxx"
41
+ redmine_api_key = "yyy"
42
+ default_project_id = "1"
43
+ wiki_project_id = "2"
44
+ editor = "nvim"
45
+
46
+ ["sub"]
47
+ redmine_url = "vvv"
48
+ redmine_api_key = "www"
49
+ default_project_id = "2"
50
+ wiki_project_id = "3"
51
+ editor = "code"
52
+ ```
53
+
54
+ ### setup completion
55
+
56
+ ```sh
57
+ uv tool install argcomplete
58
+ echo 'eval "$(register-python-argcomplete redi)"' >> ~/.zshrc
59
+ ```
60
+
61
+ ## usage(example)
62
+
63
+ ```sh
64
+ # config (alias: c)
65
+ redi config
66
+ redi config create <profile_name> --url <url> --api_key <key> # create new profile
67
+ redi config create <profile_name> --url <url> --api_key <key> --set_default
68
+ redi config update --default_profile <profile_name> # switch profile
69
+ redi config update <profile_name> --editor nvim # update profile
70
+ redi --profile <profile_name> issue # 一時的にプロファイルを切り替えて実行
71
+
72
+ # project (alias: p)
73
+ redi project # list projects
74
+ redi project list # 同上 (`redi project l` / `redi p list` / `redi p l` / `redi p` も同じ)
75
+ redi project view <project_id> # view project
76
+ redi project view <project_id> --include trackers,issue_categories
77
+ redi project create <name> <identifier>
78
+ redi project create <name> <identifier> -d "description" --is_public true
79
+ redi project update <project_id> --name renamed_project
80
+
81
+ # issue (alias: i)
82
+ redi issue # list issues
83
+ redi issue -p <project_id> -a me -s open
84
+ redi issue -q <query_id>
85
+ redi issue view <issue_id>
86
+ redi issue view <issue_id> --web # view issue with web browser
87
+ redi issue view <issue_id> --include journals,attachments,relations
88
+ redi issue create # (interactive)
89
+ redi issue create "subject" -p <project_id> -t <tracker_id> -a <user_id> -d "description"
90
+ redi issue update <issue_id> # (interactive)
91
+ redi issue update <issue_id> --status_id <status_id> -n "notes"
92
+ redi issue update <issue_id> --relate relates --to <other_issue_id>
93
+ redi issue update <issue_id> --attach ./foo.png --attach ./bar.log
94
+ redi issue comment <issue_id> "hello~"
95
+ redi issue delete <issue_id> # (confirm before delete)
96
+ redi issue delete <issue_id> -y # skip confirmation
97
+
98
+ # version (alias: v)
99
+ redi version # list versions(fixed_versions)
100
+ redi version -p <project_id>
101
+ redi version view <version_id>
102
+ redi version create <name> -p <project_id> --due_date 2026-12-31 --status open
103
+ redi version update <version_id> --status closed
104
+
105
+ # wiki (alias: w)
106
+ redi wiki
107
+ redi wiki -p <project_id>
108
+ redi wiki view <page_title>
109
+ redi wiki create # (interactive)
110
+ redi wiki update # (interactive)
111
+
112
+ # file (プロジェクトファイル)
113
+ redi file -p <project_id> # list
114
+ redi file create ./foo.zip -p <project_id> -d "description"
115
+
116
+ # attachment
117
+ redi attachment view <attachment_id>
118
+ redi attachment update <attachment_id> -f new_name.png -d "desc"
119
+ redi attachment delete <attachment_id> # confirm before delete (-y to skip)
120
+
121
+ # relation (イシュー関係性詳細)
122
+ redi relation view <relation_id>
123
+
124
+ # time_entry (作業時間)
125
+ redi time_entry -p <project_id> -u me
126
+ redi time_entry create 1.5 -i <issue_id> -a <activity_id> -c "comment"
127
+ redi time_entry update <time_entry_id> --hours 2.0
128
+ redi time_entry delete <time_entry_id> # confirm before delete (-y to skip)
129
+
130
+ # me (自分のアカウント)
131
+ redi me
132
+ redi me update -f <firstname> -l <lastname> -m <mail>
133
+
134
+ # membership (alias: m)
135
+ redi membership -p <project_id>
136
+ redi membership view <membership_id>
137
+
138
+ # news
139
+ redi news -p <project_id>
140
+
141
+ # issue_category
142
+ redi issue_category -p <project_id>
143
+ redi issue_category create "category" -p <project_id>
144
+
145
+ # others
146
+ redi user # list users (alias: u)
147
+ redi tracker # list trackers
148
+ redi issue_status # list issue statuses
149
+ redi issue_priority # list priorities
150
+ redi time_entry_activity # list activities
151
+ redi document_category # list document categories
152
+ redi role # list roles
153
+ redi group # list groups
154
+ redi custom_field # list custom fields
155
+ redi query # list custom queries
156
+ redi search "keyword"
157
+ redi --version
158
+ redi --tui
159
+ ```
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "redtile"
3
+ version = "0.0.17"
4
+ description = "Redmine CLI tool"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "kawagh", email = "kawagh.dev@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "argcomplete>=3.6.3",
12
+ "prompt-toolkit>=3.0.52",
13
+ "requests>=2.33.1",
14
+ "tomlkit>=0.14.0",
15
+ "wcwidth>=0.2.13",
16
+ ]
17
+
18
+ [project.scripts]
19
+ redi = "redi.cli:main"
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.9.2,<0.10.0"]
23
+ build-backend = "uv_build"
24
+
25
+ [dependency-groups]
26
+ dev = [
27
+ "pytest>=9.0.3",
28
+ "ruff>=0.15.9",
29
+ "ty>=0.0.31",
30
+ ]
31
+
32
+ [tool.uv.build-backend]
33
+ module-name = "redi"
File without changes
File without changes
@@ -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)
@@ -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']}")