jira820 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 (37) hide show
  1. jira820-0.1.0/.github/workflows/ci.yml +25 -0
  2. jira820-0.1.0/.github/workflows/publish.yml +27 -0
  3. jira820-0.1.0/.gitignore +26 -0
  4. jira820-0.1.0/CHANGELOG.md +15 -0
  5. jira820-0.1.0/LICENSE +21 -0
  6. jira820-0.1.0/PKG-INFO +159 -0
  7. jira820-0.1.0/README.md +135 -0
  8. jira820-0.1.0/examples/config.yaml +55 -0
  9. jira820-0.1.0/examples/curl-examples.sh +40 -0
  10. jira820-0.1.0/pyproject.toml +42 -0
  11. jira820-0.1.0/src/jira820/__init__.py +14 -0
  12. jira820-0.1.0/src/jira820/__main__.py +4 -0
  13. jira820-0.1.0/src/jira820/agile.py +77 -0
  14. jira820-0.1.0/src/jira820/atom.py +73 -0
  15. jira820-0.1.0/src/jira820/cli.py +23 -0
  16. jira820-0.1.0/src/jira820/config.py +163 -0
  17. jira820-0.1.0/src/jira820/content/__init__.py +9 -0
  18. jira820-0.1.0/src/jira820/content/en.py +59 -0
  19. jira820-0.1.0/src/jira820/content/ko.py +55 -0
  20. jira820-0.1.0/src/jira820/jql.py +191 -0
  21. jira820-0.1.0/src/jira820/serialize.py +226 -0
  22. jira820-0.1.0/src/jira820/server.py +40 -0
  23. jira820-0.1.0/src/jira820/server_agile.py +150 -0
  24. jira820-0.1.0/src/jira820/server_read.py +284 -0
  25. jira820-0.1.0/src/jira820/server_write.py +88 -0
  26. jira820-0.1.0/src/jira820/store.py +465 -0
  27. jira820-0.1.0/src/jira820/workflow.py +66 -0
  28. jira820-0.1.0/src/jira820/world.py +340 -0
  29. jira820-0.1.0/tests/conftest.py +30 -0
  30. jira820-0.1.0/tests/test_agile.py +74 -0
  31. jira820-0.1.0/tests/test_atom.py +24 -0
  32. jira820-0.1.0/tests/test_config.py +39 -0
  33. jira820-0.1.0/tests/test_jql.py +53 -0
  34. jira820-0.1.0/tests/test_persist.py +26 -0
  35. jira820-0.1.0/tests/test_read.py +50 -0
  36. jira820-0.1.0/tests/test_workflow.py +37 -0
  37. jira820-0.1.0/tests/test_write.py +65 -0
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: |
22
+ python -m pip install --upgrade pip
23
+ pip install -e ".[test]"
24
+ - name: Test
25
+ run: pytest -q
@@ -0,0 +1,27 @@
1
+ name: Publish to PyPI
2
+
3
+ # 태그(v*) 푸시 또는 GitHub Release 발행 시 PyPI 로 배포.
4
+ # PyPI Trusted Publishing(OIDC) 사용 — API 토큰 불필요.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi # PyPI Trusted Publisher 의 'Environment name' 과 일치해야 함
15
+ permissions:
16
+ id-token: write # OIDC 토큰 발급용 (필수)
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - name: Build sdist + wheel
23
+ run: |
24
+ python -m pip install --upgrade pip build
25
+ python -m build
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Test / tooling
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Mock runtime state (JIRA820_PERSIST)
20
+ *.state.json
21
+ state.json
22
+
23
+ # Editor / OS
24
+ .idea/
25
+ .vscode/
26
+ .DS_Store
@@ -0,0 +1,15 @@
1
+ # 변경 이력
2
+
3
+ ## 0.1.0
4
+
5
+ 첫 릴리스.
6
+
7
+ - Jira Data Center 8.20.8 의 상태형 mock: REST v2(읽기+쓰기)와 Agile `/rest/agile/1.0/`.
8
+ - 결정적 시드 데이터셋(사용자, Epic → Story/Task/Bug → Sub-task, 코멘트, 워크로그, 버전, activity ATOM,
9
+ Confluence 페이지). `JIRA820_*` 환경변수 + YAML 로 설정.
10
+ - 쓰기: 이슈 생성/수정/삭제, 전이(resolution + changelog), 코멘트, 워크로그, 담당자.
11
+ - Agile: 시드된 Scrum 보드 + 스프린트(closed/active/future), Kanban 보드 + 컬럼; 스프린트/백로그 이동,
12
+ 스프린트 생성/시작; 칸반 컬럼 이동 = 전이.
13
+ - 확장 JQL, 파일 영속화(`JIRA820_PERSIST`), 읽기전용 모드(`JIRA820_READONLY`), 영어/한국어 로케일.
14
+ - 외부 데이터 주입 훅(`Store(config, seed=False)`, `config.subtask_type`) — 다른 프로젝트의 world 를 주입해
15
+ 이 패키지의 서버로 서빙 가능.
jira820-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dongyi-kim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jira820-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: jira820
3
+ Version: 0.1.0
4
+ Summary: Jira Data Center 8.20.8 을 로컬에서 흉내 내는 상태형 읽기+쓰기 mock 서버 (REST v2 + Agile 보드/스프린트/칸반).
5
+ Project-URL: Homepage, https://github.com/dongyi-kim/jira820
6
+ Project-URL: Issues, https://github.com/dongyi-kim/jira820/issues
7
+ Author: dongyi-kim
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agile,fake,jira,jira-data-center,kanban,mock,rest-api,sprint,testing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing :: Mocking
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: fastapi>=0.110
18
+ Requires-Dist: pyyaml>=6.0
19
+ Requires-Dist: uvicorn[standard]>=0.29
20
+ Provides-Extra: test
21
+ Requires-Dist: httpx>=0.27; extra == 'test'
22
+ Requires-Dist: pytest>=7; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # jira820
26
+
27
+ **Jira Data Center 8.20.8** 을 로컬에서 흉내 내는 **상태형(stateful) 읽기+쓰기 mock 서버**입니다.
28
+ REST v2 는 물론 Agile(`/rest/agile/1.0/`) API 까지 — **실제 Scrum 스프린트와 Kanban 보드**를 포함합니다.
29
+ 결정적으로 생성된 그럴듯한 프로젝트 데이터를 서빙하고 **변경(생성/수정/전이/코멘트/워크로그, 스프린트·백로그
30
+ 이동)까지 받아들이므로**, Jira 인스턴스·라이선스·DB 없이 **티켓 뷰어 *및* 작성 클라이언트**를 개발·테스트할 수 있습니다.
31
+
32
+ ```bash
33
+ pip install jira820 # (PyPI 등록 후)
34
+ # 또는 저장소에서 직접:
35
+ pip install "jira820 @ git+https://github.com/dongyi-kim/jira820@v0.1.0"
36
+
37
+ jira820 # -> http://127.0.0.1:8080
38
+ ```
39
+
40
+ ## 왜 만들었나
41
+
42
+ Jira **Data Center 는 신규 트라이얼 라이선스 발급이 중단**되어, 개발 대상인 실제 8.20.x 서버를 새로 띄우기가
43
+ 현실적으로 어렵습니다(구 Docker 이미지는 있으나 새로 발급 못 받는 DC 라이선스 키가 필요). 기존 목(mock) 도구들도
44
+ 이 공백을 못 메웁니다:
45
+
46
+ | 도구 | 상태 유지 | JQL 평가 | Epic→자식/롤업 | 쓰기 | Agile 보드/스프린트 |
47
+ |------|:---:|:---:|:---:|:---:|:---:|
48
+ | Mockoon / WireMock / MockServer / Prism | ❌ 고정응답 | ❌ | ❌ | ❌ | ❌ |
49
+ | `pycontribs/jira` | — (클라이언트) | — | — | — | — |
50
+ | Atlassian `MockitoContainer` | JVM 내부만 | ❌ | ❌ | ❌ | ❌ |
51
+ | **jira820** | ✅ | ✅ | ✅ | ✅ | ✅ |
52
+
53
+ 정적 목 도구는 고정 응답을 재생할 뿐이고, 이 프로젝트는 **살아있는 관계형·가변 데이터 모델**을 유지합니다.
54
+
55
+ ## 실행
56
+
57
+ ```bash
58
+ pip install -e . # 체크아웃에서
59
+ jira820 # 콘솔 스크립트
60
+ python -m jira820 # 모듈 형태
61
+
62
+ JIRA820_PORT=9000 JIRA820_LOCALE=ko JIRA820_SEED=7 jira820
63
+ JIRA820_CONFIG=examples/config.yaml jira820
64
+ ```
65
+
66
+ ## 설정 (`JIRA820_*` 환경변수 > `JIRA820_CONFIG` YAML > 기본값)
67
+
68
+ | 변수 | 기본값 | 용도 |
69
+ |---|---|---|
70
+ | `JIRA820_HOST` / `JIRA820_PORT` | `127.0.0.1` / `8080` | 바인드 주소 |
71
+ | `JIRA820_LATENCY_MS` | `0` | 요청당 인위적 지연(캐시/성능 테스트) |
72
+ | `JIRA820_SEED` | `0` | (결정적) 데이터셋 변주 |
73
+ | `JIRA820_DATE` | 오늘 | 모든 상대 날짜의 "오늘" 기준 |
74
+ | `JIRA820_PROJECT_KEY` / `JIRA820_PROJECT_NAME` | `DEMO` / `Demo Project` | 프로젝트 식별 |
75
+ | `JIRA820_LOCALE` | `en` | `en` 또는 `ko` |
76
+ | `JIRA820_SP_FIELD` / `JIRA820_EPIC_LINK_FIELD` / `JIRA820_SPRINT_FIELD` | `customfield_10004/10008/10007` | 커스텀필드 id |
77
+ | `JIRA820_SUBTASK_TYPE` | `Sub-task` | `issuetype.subtask` 판정에 쓰는 서브태스크 타입명 |
78
+ | `JIRA820_SERVER_VERSION` | `8.20.8` | `serverInfo` 버전 |
79
+ | `JIRA820_READONLY` | `false` | 쓰기를 `403` 으로 차단 |
80
+ | `JIRA820_PERSIST` | — | 가변 상태를 JSON 파일로 로드/저장(재시작 간 유지) |
81
+ | `JIRA820_CONFIG` | — | 더 풍부한 커스터마이즈 YAML (`examples/config.yaml` 참고) |
82
+
83
+ 우선순위는 **환경변수 → YAML → 기본값**. 데이터셋은 `(seed, date)` 가 같으면 항상 동일합니다.
84
+
85
+ ## 엔드포인트
86
+
87
+ **읽기** — `serverInfo`, `myself`, `user`, `field`, `status`, `issuetype`, `priority`, `resolution`,
88
+ `project`(+`/{key}`, `/components`, `/versions`, `/statuses`), `search`(JQL + `fields` 투영 + 페이징),
89
+ `issue/{key}`(`?expand=changelog`), `issue/{key}/comment`, `issue/{key}/worklog`, `issue/{key}/transitions`,
90
+ `issue/createmeta`, `issue/{key}/editmeta`, `activity`(ATOM), `content/search`(Confluence CQL).
91
+
92
+ **쓰기** — `POST issue`, `PUT issue/{key}`, `DELETE issue/{key}`, `POST issue/{key}/transitions`,
93
+ `POST/PUT/DELETE issue/{key}/comment[/{id}]`, `POST issue/{key}/worklog`, `PUT issue/{key}/assignee`.
94
+
95
+ **Agile** (`/rest/agile/1.0/`) — `board`(scrum+kanban), `board/{id}`(+`/configuration`, `/issue`, `/backlog`,
96
+ `/sprint`, `/epic`), `epic/{key}/issue`, `sprint/{id}`(+`/issue`), `POST sprint`, `PUT sprint/{id}`(시작/완료),
97
+ `POST sprint/{id}/issue`, `POST backlog/issue`.
98
+
99
+ 전이(transition)는 관대한 오픈 워크플로입니다. *done* 카테고리 상태로 들어가면 resolution 과 `resolutiondate` 가
100
+ 세팅되고 changelog 가 기록되며, 벗어나면 해제됩니다. **Kanban 컬럼 이동 = 그 컬럼에 매핑된 상태로의 전이**입니다.
101
+
102
+ ## 클라이언트에서 사용
103
+
104
+ ```python
105
+ import requests
106
+ BASE = "http://127.0.0.1:8080"
107
+ # 생성
108
+ key = requests.post(f"{BASE}/rest/api/2/issue",
109
+ json={"fields": {"project": {"key": "DEMO"}, "issuetype": {"name": "Task"},
110
+ "summary": "무언가 배포"}}).json()["key"]
111
+ # 전이
112
+ t = requests.get(f"{BASE}/rest/api/2/issue/{key}/transitions").json()["transitions"][0]["id"]
113
+ requests.post(f"{BASE}/rest/api/2/issue/{key}/transitions", json={"transition": {"id": t}})
114
+ # 활성 스프린트로 이동
115
+ sid = requests.get(f"{BASE}/rest/agile/1.0/board/1/sprint?state=active").json()["values"][0]["id"]
116
+ requests.post(f"{BASE}/rest/agile/1.0/sprint/{sid}/issue", json={"issues": [key]})
117
+ ```
118
+
119
+ 전체 셸 예시는 `examples/curl-examples.sh` 를 보세요.
120
+
121
+ ## 내 데이터 주입(임베드)
122
+
123
+ ```python
124
+ from jira820 import make_app, build_store
125
+ from jira820.config import Config
126
+ from jira820.store import Store
127
+
128
+ app = make_app() # 기본 시드 스토어(ASGI 앱)
129
+ # 또는 외부 데이터 주입: 빈 스토어를 만들고 채워서 넘김
130
+ store = Store(Config(project_key="DL"), seed=False)
131
+ store.issues = my_issues; store.users = my_users; store.reindex()
132
+ app = make_app(store=store) # 이 패키지의 직렬화기/엔드포인트를 그대로 재사용
133
+ ```
134
+
135
+ ## 개발
136
+
137
+ ```bash
138
+ pip install -e ".[test]"
139
+ pytest -q
140
+ ```
141
+
142
+ ## PyPI 배포 (Trusted Publishing, 토큰 불필요)
143
+
144
+ `.github/workflows/publish.yml` 가 태그(`v*`) 푸시 시 **PyPI Trusted Publishing(OIDC)** 로 자동 배포합니다.
145
+ 최초 1회 PyPI 에서 Trusted Publisher 를 등록하세요: Project `jira820` / Owner `dongyi-kim` / Repository `jira820`
146
+ / Workflow `publish.yml` / Environment `pypi`.
147
+
148
+ ## 참고 / 한계
149
+
150
+ - 쓰기 모델은 관대한 단일 테넌트 인메모리 근사입니다 — 클라이언트 구동엔 훌륭하지만 모든 워크플로/권한 규칙을
151
+ 충실히 재현하진 않습니다.
152
+ - JQL 은 흔한 형태(`=,!=,>=,<=,>,<,IN,~`, `AND`/`OR`, `ORDER BY`, project/assignee/status/statusCategory/type/
153
+ labels/sprint/날짜범위)를 지원하며, 인식하지 못한 절은 오류 대신 무시합니다.
154
+ - Atlassian 과 무관합니다. "Jira" 는 Atlassian 의 상표이며, 이 프로젝트는 로컬 개발·테스트를 위해 공개 REST 형태만
155
+ 흉내 냅니다.
156
+
157
+ ## 라이선스
158
+
159
+ MIT — [LICENSE](LICENSE) 참고.
@@ -0,0 +1,135 @@
1
+ # jira820
2
+
3
+ **Jira Data Center 8.20.8** 을 로컬에서 흉내 내는 **상태형(stateful) 읽기+쓰기 mock 서버**입니다.
4
+ REST v2 는 물론 Agile(`/rest/agile/1.0/`) API 까지 — **실제 Scrum 스프린트와 Kanban 보드**를 포함합니다.
5
+ 결정적으로 생성된 그럴듯한 프로젝트 데이터를 서빙하고 **변경(생성/수정/전이/코멘트/워크로그, 스프린트·백로그
6
+ 이동)까지 받아들이므로**, Jira 인스턴스·라이선스·DB 없이 **티켓 뷰어 *및* 작성 클라이언트**를 개발·테스트할 수 있습니다.
7
+
8
+ ```bash
9
+ pip install jira820 # (PyPI 등록 후)
10
+ # 또는 저장소에서 직접:
11
+ pip install "jira820 @ git+https://github.com/dongyi-kim/jira820@v0.1.0"
12
+
13
+ jira820 # -> http://127.0.0.1:8080
14
+ ```
15
+
16
+ ## 왜 만들었나
17
+
18
+ Jira **Data Center 는 신규 트라이얼 라이선스 발급이 중단**되어, 개발 대상인 실제 8.20.x 서버를 새로 띄우기가
19
+ 현실적으로 어렵습니다(구 Docker 이미지는 있으나 새로 발급 못 받는 DC 라이선스 키가 필요). 기존 목(mock) 도구들도
20
+ 이 공백을 못 메웁니다:
21
+
22
+ | 도구 | 상태 유지 | JQL 평가 | Epic→자식/롤업 | 쓰기 | Agile 보드/스프린트 |
23
+ |------|:---:|:---:|:---:|:---:|:---:|
24
+ | Mockoon / WireMock / MockServer / Prism | ❌ 고정응답 | ❌ | ❌ | ❌ | ❌ |
25
+ | `pycontribs/jira` | — (클라이언트) | — | — | — | — |
26
+ | Atlassian `MockitoContainer` | JVM 내부만 | ❌ | ❌ | ❌ | ❌ |
27
+ | **jira820** | ✅ | ✅ | ✅ | ✅ | ✅ |
28
+
29
+ 정적 목 도구는 고정 응답을 재생할 뿐이고, 이 프로젝트는 **살아있는 관계형·가변 데이터 모델**을 유지합니다.
30
+
31
+ ## 실행
32
+
33
+ ```bash
34
+ pip install -e . # 체크아웃에서
35
+ jira820 # 콘솔 스크립트
36
+ python -m jira820 # 모듈 형태
37
+
38
+ JIRA820_PORT=9000 JIRA820_LOCALE=ko JIRA820_SEED=7 jira820
39
+ JIRA820_CONFIG=examples/config.yaml jira820
40
+ ```
41
+
42
+ ## 설정 (`JIRA820_*` 환경변수 > `JIRA820_CONFIG` YAML > 기본값)
43
+
44
+ | 변수 | 기본값 | 용도 |
45
+ |---|---|---|
46
+ | `JIRA820_HOST` / `JIRA820_PORT` | `127.0.0.1` / `8080` | 바인드 주소 |
47
+ | `JIRA820_LATENCY_MS` | `0` | 요청당 인위적 지연(캐시/성능 테스트) |
48
+ | `JIRA820_SEED` | `0` | (결정적) 데이터셋 변주 |
49
+ | `JIRA820_DATE` | 오늘 | 모든 상대 날짜의 "오늘" 기준 |
50
+ | `JIRA820_PROJECT_KEY` / `JIRA820_PROJECT_NAME` | `DEMO` / `Demo Project` | 프로젝트 식별 |
51
+ | `JIRA820_LOCALE` | `en` | `en` 또는 `ko` |
52
+ | `JIRA820_SP_FIELD` / `JIRA820_EPIC_LINK_FIELD` / `JIRA820_SPRINT_FIELD` | `customfield_10004/10008/10007` | 커스텀필드 id |
53
+ | `JIRA820_SUBTASK_TYPE` | `Sub-task` | `issuetype.subtask` 판정에 쓰는 서브태스크 타입명 |
54
+ | `JIRA820_SERVER_VERSION` | `8.20.8` | `serverInfo` 버전 |
55
+ | `JIRA820_READONLY` | `false` | 쓰기를 `403` 으로 차단 |
56
+ | `JIRA820_PERSIST` | — | 가변 상태를 JSON 파일로 로드/저장(재시작 간 유지) |
57
+ | `JIRA820_CONFIG` | — | 더 풍부한 커스터마이즈 YAML (`examples/config.yaml` 참고) |
58
+
59
+ 우선순위는 **환경변수 → YAML → 기본값**. 데이터셋은 `(seed, date)` 가 같으면 항상 동일합니다.
60
+
61
+ ## 엔드포인트
62
+
63
+ **읽기** — `serverInfo`, `myself`, `user`, `field`, `status`, `issuetype`, `priority`, `resolution`,
64
+ `project`(+`/{key}`, `/components`, `/versions`, `/statuses`), `search`(JQL + `fields` 투영 + 페이징),
65
+ `issue/{key}`(`?expand=changelog`), `issue/{key}/comment`, `issue/{key}/worklog`, `issue/{key}/transitions`,
66
+ `issue/createmeta`, `issue/{key}/editmeta`, `activity`(ATOM), `content/search`(Confluence CQL).
67
+
68
+ **쓰기** — `POST issue`, `PUT issue/{key}`, `DELETE issue/{key}`, `POST issue/{key}/transitions`,
69
+ `POST/PUT/DELETE issue/{key}/comment[/{id}]`, `POST issue/{key}/worklog`, `PUT issue/{key}/assignee`.
70
+
71
+ **Agile** (`/rest/agile/1.0/`) — `board`(scrum+kanban), `board/{id}`(+`/configuration`, `/issue`, `/backlog`,
72
+ `/sprint`, `/epic`), `epic/{key}/issue`, `sprint/{id}`(+`/issue`), `POST sprint`, `PUT sprint/{id}`(시작/완료),
73
+ `POST sprint/{id}/issue`, `POST backlog/issue`.
74
+
75
+ 전이(transition)는 관대한 오픈 워크플로입니다. *done* 카테고리 상태로 들어가면 resolution 과 `resolutiondate` 가
76
+ 세팅되고 changelog 가 기록되며, 벗어나면 해제됩니다. **Kanban 컬럼 이동 = 그 컬럼에 매핑된 상태로의 전이**입니다.
77
+
78
+ ## 클라이언트에서 사용
79
+
80
+ ```python
81
+ import requests
82
+ BASE = "http://127.0.0.1:8080"
83
+ # 생성
84
+ key = requests.post(f"{BASE}/rest/api/2/issue",
85
+ json={"fields": {"project": {"key": "DEMO"}, "issuetype": {"name": "Task"},
86
+ "summary": "무언가 배포"}}).json()["key"]
87
+ # 전이
88
+ t = requests.get(f"{BASE}/rest/api/2/issue/{key}/transitions").json()["transitions"][0]["id"]
89
+ requests.post(f"{BASE}/rest/api/2/issue/{key}/transitions", json={"transition": {"id": t}})
90
+ # 활성 스프린트로 이동
91
+ sid = requests.get(f"{BASE}/rest/agile/1.0/board/1/sprint?state=active").json()["values"][0]["id"]
92
+ requests.post(f"{BASE}/rest/agile/1.0/sprint/{sid}/issue", json={"issues": [key]})
93
+ ```
94
+
95
+ 전체 셸 예시는 `examples/curl-examples.sh` 를 보세요.
96
+
97
+ ## 내 데이터 주입(임베드)
98
+
99
+ ```python
100
+ from jira820 import make_app, build_store
101
+ from jira820.config import Config
102
+ from jira820.store import Store
103
+
104
+ app = make_app() # 기본 시드 스토어(ASGI 앱)
105
+ # 또는 외부 데이터 주입: 빈 스토어를 만들고 채워서 넘김
106
+ store = Store(Config(project_key="DL"), seed=False)
107
+ store.issues = my_issues; store.users = my_users; store.reindex()
108
+ app = make_app(store=store) # 이 패키지의 직렬화기/엔드포인트를 그대로 재사용
109
+ ```
110
+
111
+ ## 개발
112
+
113
+ ```bash
114
+ pip install -e ".[test]"
115
+ pytest -q
116
+ ```
117
+
118
+ ## PyPI 배포 (Trusted Publishing, 토큰 불필요)
119
+
120
+ `.github/workflows/publish.yml` 가 태그(`v*`) 푸시 시 **PyPI Trusted Publishing(OIDC)** 로 자동 배포합니다.
121
+ 최초 1회 PyPI 에서 Trusted Publisher 를 등록하세요: Project `jira820` / Owner `dongyi-kim` / Repository `jira820`
122
+ / Workflow `publish.yml` / Environment `pypi`.
123
+
124
+ ## 참고 / 한계
125
+
126
+ - 쓰기 모델은 관대한 단일 테넌트 인메모리 근사입니다 — 클라이언트 구동엔 훌륭하지만 모든 워크플로/권한 규칙을
127
+ 충실히 재현하진 않습니다.
128
+ - JQL 은 흔한 형태(`=,!=,>=,<=,>,<,IN,~`, `AND`/`OR`, `ORDER BY`, project/assignee/status/statusCategory/type/
129
+ labels/sprint/날짜범위)를 지원하며, 인식하지 못한 절은 오류 대신 무시합니다.
130
+ - Atlassian 과 무관합니다. "Jira" 는 Atlassian 의 상표이며, 이 프로젝트는 로컬 개발·테스트를 위해 공개 REST 형태만
131
+ 흉내 냅니다.
132
+
133
+ ## 라이선스
134
+
135
+ MIT — [LICENSE](LICENSE) 참고.
@@ -0,0 +1,55 @@
1
+ # Example jira820 config. Point at it with JIRA820_CONFIG=examples/config.yaml
2
+ # Any JIRA820_* environment variable overrides the matching key here.
3
+
4
+ # --- server ---
5
+ host: 127.0.0.1
6
+ port: 8080
7
+ latency_ms: 0 # inject artificial latency per request (cache/perf testing)
8
+
9
+ # --- dataset identity & determinism ---
10
+ seed: "0" # change to get a different (but still deterministic) dataset
11
+ date: 2026-01-15 # "today" anchor for all relative dates (reproducible)
12
+ project_key: DEMO
13
+ project_name: Demo Project
14
+ server_version: 8.20.8
15
+ locale: en # en | ko
16
+
17
+ # --- custom field ids (match what your client expects) ---
18
+ sp_field: customfield_10004
19
+ epic_link_field: customfield_10008
20
+ sprint_field: customfield_10007
21
+ subtask_type: Sub-task # issue-type name treated as a sub-task (issuetype.subtask=true)
22
+
23
+ # --- behaviour ---
24
+ readonly: false # set true to reject all writes with 403
25
+ # persist: state.json # uncomment to load/save mutable state to a file across restarts
26
+
27
+ # --- dataset shape ---
28
+ modules: [Web, API, Mobile, Payments, Platform, Infra]
29
+ components_extra: [Support]
30
+ users_per_module: 3
31
+
32
+ # volume knobs
33
+ epics_per_module: 3
34
+ children_per_epic: [4, 9]
35
+ standalone_per_module: 10
36
+ history_per_module: 12
37
+ sprints_per_scrum_board: 5 # a few closed + one active + a couple future
38
+
39
+ # workflow (statusName, category[todo|inprogress|done], numeric id)
40
+ statuses:
41
+ - [Open, todo, "1"]
42
+ - [In Progress, inprogress, "3"]
43
+ - [In Review, inprogress, "10001"]
44
+ - [Done, done, "10002"]
45
+ - [Reopened, todo, "4"]
46
+
47
+ # issue types (name, numeric id). A type literally named "Sub-task" is treated as a subtask.
48
+ issue_types:
49
+ - [Bug, "1"]
50
+ - [Epic, "2"]
51
+ - [Improvement, "3"]
52
+ - [New Feature, "4"]
53
+ - [Story, "5"]
54
+ - [Task, "6"]
55
+ - [Sub-task, "7"]
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # Example requests against a running jira820 (default http://127.0.0.1:8080).
3
+ set -euo pipefail
4
+ BASE=${BASE:-http://127.0.0.1:8080}
5
+ PK=${PK:-DEMO}
6
+
7
+ echo "# server info"
8
+ curl -s "$BASE/rest/api/2/serverInfo" | python -m json.tool
9
+
10
+ echo "# search (JQL) with field projection + paging"
11
+ curl -s "$BASE/rest/api/2/search?jql=project=$PK%20ORDER%20BY%20updated%20DESC&maxResults=3&fields=summary,status,assignee" | python -m json.tool
12
+
13
+ echo "# one issue with changelog"
14
+ curl -s "$BASE/rest/api/2/issue/$PK-1?expand=changelog" | python -m json.tool
15
+
16
+ echo "# agile: boards (scrum + kanban)"
17
+ curl -s "$BASE/rest/agile/1.0/board" | python -m json.tool
18
+
19
+ echo "# agile: kanban board columns"
20
+ curl -s "$BASE/rest/agile/1.0/board/2/configuration" | python -m json.tool
21
+
22
+ echo "# --- WRITE: create an issue ---"
23
+ KEY=$(curl -s -X POST "$BASE/rest/api/2/issue" -H 'Content-Type: application/json' \
24
+ -d "{\"fields\":{\"project\":{\"key\":\"$PK\"},\"issuetype\":{\"name\":\"Task\"},\"summary\":\"created via curl\"}}" \
25
+ | python -c "import sys,json;print(json.load(sys.stdin)['key'])")
26
+ echo "created $KEY"
27
+
28
+ echo "# available transitions, then move to first one"
29
+ TID=$(curl -s "$BASE/rest/api/2/issue/$KEY/transitions" | python -c "import sys,json;print(json.load(sys.stdin)['transitions'][0]['id'])")
30
+ curl -s -X POST "$BASE/rest/api/2/issue/$KEY/transitions" -H 'Content-Type: application/json' -d "{\"transition\":{\"id\":\"$TID\"}}"
31
+ echo "transitioned $KEY via $TID"
32
+
33
+ echo "# add a comment + worklog"
34
+ curl -s -X POST "$BASE/rest/api/2/issue/$KEY/comment" -H 'Content-Type: application/json' -d '{"body":"nice"}' >/dev/null
35
+ curl -s -X POST "$BASE/rest/api/2/issue/$KEY/worklog" -H 'Content-Type: application/json' -d '{"timeSpentSeconds":3600}' >/dev/null
36
+
37
+ echo "# move it into the active sprint (board 1)"
38
+ SID=$(curl -s "$BASE/rest/agile/1.0/board/1/sprint?state=active" | python -c "import sys,json;print(json.load(sys.stdin)['values'][0]['id'])")
39
+ curl -s -X POST "$BASE/rest/agile/1.0/sprint/$SID/issue" -H 'Content-Type: application/json' -d "{\"issues\":[\"$KEY\"]}"
40
+ echo "moved $KEY into sprint $SID"
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jira820"
7
+ version = "0.1.0"
8
+ description = "Jira Data Center 8.20.8 을 로컬에서 흉내 내는 상태형 읽기+쓰기 mock 서버 (REST v2 + Agile 보드/스프린트/칸반)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "dongyi-kim" }]
13
+ keywords = ["jira", "mock", "fake", "testing", "jira-data-center", "rest-api", "agile", "sprint", "kanban"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Testing :: Mocking",
20
+ ]
21
+ dependencies = [
22
+ "fastapi>=0.110",
23
+ "uvicorn[standard]>=0.29",
24
+ "PyYAML>=6.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ test = ["pytest>=7", "httpx>=0.27"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/dongyi-kim/jira820"
32
+ Issues = "https://github.com/dongyi-kim/jira820/issues"
33
+
34
+ [project.scripts]
35
+ jira820 = "jira820.cli:main"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/jira820"]
39
+
40
+ [tool.pytest.ini_options]
41
+ pythonpath = ["src"]
42
+ testpaths = ["tests"]
@@ -0,0 +1,14 @@
1
+ """jira-dc-8.20-mock: a stateful, read+write mock of Jira Data Center 8.20.8.
2
+
3
+ Public API:
4
+ make_app(store=None, config=None) -> FastAPI # build the ASGI app
5
+ build_store(config=None) -> Store # build/seed a data store
6
+ load_config() -> Config # read JIRA820_* env + YAML
7
+ """
8
+
9
+ from .config import Config, load_config
10
+ from .server import build_store, make_app
11
+ from .store import JiraError, Store
12
+
13
+ __version__ = "0.1.0"
14
+ __all__ = ["make_app", "build_store", "load_config", "Config", "Store", "JiraError", "__version__"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,77 @@
1
+ """Agile board read-model: which issues belong to a board, its backlog, sprints, epics, columns.
2
+
3
+ Boards filter on the project. Scrum backlog = board issues not in an active/future sprint.
4
+ Kanban board issues = all non-subtask issues; columns map to statuses (see workflow.kanban_columns).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ def _board_issue_keys(store, board_id: int) -> list:
11
+ board = store.boards.get(board_id)
12
+ if not board:
13
+ return []
14
+ pk = board["projectKey"]
15
+ keys = [k for k, it in store.issues.items()
16
+ if it["project"] == pk and it["type"] not in ("Epic", "Sub-task")]
17
+ keys.sort()
18
+ return keys
19
+
20
+
21
+ def board_issue_keys(store, board_id: int) -> list:
22
+ return _board_issue_keys(store, board_id)
23
+
24
+
25
+ def backlog_keys(store, board_id: int) -> list:
26
+ """Issues not currently in an active/future sprint (and not done)."""
27
+ active_future = {s["id"] for s in store.sprints.values()
28
+ if s["boardId"] == board_id and s["state"] in ("active", "future")}
29
+ out = []
30
+ for k in _board_issue_keys(store, board_id):
31
+ it = store.issues[k]
32
+ in_open_sprint = any(sid in active_future for sid in it.get("sprints", []))
33
+ if not in_open_sprint and it["statusCategory"] != "done":
34
+ out.append(k)
35
+ return out
36
+
37
+
38
+ def board_sprint_ids(store, board_id: int) -> list:
39
+ ids = [sid for sid, s in store.sprints.items() if s["boardId"] == board_id]
40
+ ids.sort()
41
+ return ids
42
+
43
+
44
+ def board_epic_keys(store, board_id: int) -> list:
45
+ board = store.boards.get(board_id)
46
+ if not board:
47
+ return []
48
+ pk = board["projectKey"]
49
+ return sorted(k for k, it in store.issues.items() if it["project"] == pk and it["type"] == "Epic")
50
+
51
+
52
+ def sprint_issue_keys(store, sid: int) -> list:
53
+ return sorted(k for k, it in store.issues.items() if sid in it.get("sprints", []))
54
+
55
+
56
+ def board_json(store, board_id: int, base_url: str) -> dict:
57
+ b = store.boards[board_id]
58
+ return {"id": b["id"], "self": f"{base_url}/rest/agile/1.0/board/{b['id']}",
59
+ "name": b["name"], "type": b["type"],
60
+ "location": {"projectKey": b["projectKey"], "projectName": store.config.project_name,
61
+ "projectTypeKey": "software"}}
62
+
63
+
64
+ def board_configuration(store, board_id: int, base_url: str) -> dict:
65
+ b = store.boards[board_id]
66
+ ser = store.serializer
67
+ columns = store.workflow.kanban_columns(ser)
68
+ return {
69
+ "id": b["id"], "name": b["name"], "type": b["type"],
70
+ "self": f"{base_url}/rest/agile/1.0/board/{b['id']}/configuration",
71
+ "location": {"type": "project", "key": b["projectKey"]},
72
+ "filter": {"id": str(b["filterId"]), "self": f"{base_url}/rest/api/2/filter/{b['filterId']}"},
73
+ "columnConfig": {"constraintType": "issueCount", "columns": columns},
74
+ "estimation": {"type": "field",
75
+ "field": {"fieldId": store.config.sp_field, "displayName": "Story Points"}},
76
+ "ranking": {"rankCustomFieldId": 10005},
77
+ }