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.
- jira820-0.1.0/.github/workflows/ci.yml +25 -0
- jira820-0.1.0/.github/workflows/publish.yml +27 -0
- jira820-0.1.0/.gitignore +26 -0
- jira820-0.1.0/CHANGELOG.md +15 -0
- jira820-0.1.0/LICENSE +21 -0
- jira820-0.1.0/PKG-INFO +159 -0
- jira820-0.1.0/README.md +135 -0
- jira820-0.1.0/examples/config.yaml +55 -0
- jira820-0.1.0/examples/curl-examples.sh +40 -0
- jira820-0.1.0/pyproject.toml +42 -0
- jira820-0.1.0/src/jira820/__init__.py +14 -0
- jira820-0.1.0/src/jira820/__main__.py +4 -0
- jira820-0.1.0/src/jira820/agile.py +77 -0
- jira820-0.1.0/src/jira820/atom.py +73 -0
- jira820-0.1.0/src/jira820/cli.py +23 -0
- jira820-0.1.0/src/jira820/config.py +163 -0
- jira820-0.1.0/src/jira820/content/__init__.py +9 -0
- jira820-0.1.0/src/jira820/content/en.py +59 -0
- jira820-0.1.0/src/jira820/content/ko.py +55 -0
- jira820-0.1.0/src/jira820/jql.py +191 -0
- jira820-0.1.0/src/jira820/serialize.py +226 -0
- jira820-0.1.0/src/jira820/server.py +40 -0
- jira820-0.1.0/src/jira820/server_agile.py +150 -0
- jira820-0.1.0/src/jira820/server_read.py +284 -0
- jira820-0.1.0/src/jira820/server_write.py +88 -0
- jira820-0.1.0/src/jira820/store.py +465 -0
- jira820-0.1.0/src/jira820/workflow.py +66 -0
- jira820-0.1.0/src/jira820/world.py +340 -0
- jira820-0.1.0/tests/conftest.py +30 -0
- jira820-0.1.0/tests/test_agile.py +74 -0
- jira820-0.1.0/tests/test_atom.py +24 -0
- jira820-0.1.0/tests/test_config.py +39 -0
- jira820-0.1.0/tests/test_jql.py +53 -0
- jira820-0.1.0/tests/test_persist.py +26 -0
- jira820-0.1.0/tests/test_read.py +50 -0
- jira820-0.1.0/tests/test_workflow.py +37 -0
- 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
|
jira820-0.1.0/.gitignore
ADDED
|
@@ -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) 참고.
|
jira820-0.1.0/README.md
ADDED
|
@@ -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,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
|
+
}
|