chore: 年度リストを昇順に変更

This commit is contained in:
admin
2026-06-07 21:58:20 +09:00
parent 91632ea5b6
commit 4a500ff5e9
25 changed files with 1531 additions and 67 deletions

View File

@@ -8,6 +8,9 @@ import secrets
import os
import json
import time
import re
import io
import zipfile
from pathlib import Path
from typing import Optional
@@ -233,3 +236,243 @@ async def mark_post_done(access_token: str, post_id: str) -> dict:
)
resp.raise_for_status()
return resp.json()
# ── 申請書作成 API ─────────────────────────────────────────────
async def get_procedure_skeleton(access_token: str, proc_id: str) -> dict:
"""
手続選択:申請書スケルトン(ひな形)取得
GET /procedure/{proc_id}
Returns: {results: {file_data (base64 zip), configuration_file_name, file_info}}
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/procedure/{proc_id}",
headers={"Authorization": f"Bearer {access_token}"},
timeout=60,
)
resp.raise_for_status()
return resp.json()
async def submit_apply(access_token: str, proc_id: str,
file_name: str, file_data_b64: str,
trial: bool = True) -> dict:
"""
申請データ送信
POST /apply {"proc_id": "...", "send_file": {"file_name": "...", "file_data": "base64..."}}
Returns: {results: {arrive_id, arrive_date, proc_name, ...}}
"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
if trial:
headers["X-eGovAPI-Trial"] = "true"
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{API_BASE}/apply",
headers=headers,
json={
"proc_id": proc_id,
"send_file": {
"file_name": file_name,
"file_data": file_data_b64,
},
},
timeout=120,
)
if not resp.is_success:
try:
err_body = resp.json()
except Exception:
err_body = resp.text
raise RuntimeError(f"HTTP {resp.status_code}: {err_body}")
return resp.json()
# ── スケルトン XML 修正 ──────────────────────────────────────────
def _fill_empty_tag(text: str, tag: str, value: str) -> str:
"""kousei.xml 内の空タグ <tag/> または <tag></tag> を <tag>value</tag> に置換"""
# self-closing: <tag/> または <tag />
text = re.sub(r'<' + re.escape(tag) + r'\s*/>', f'<{tag}>{value}</{tag}>', text)
# empty: <tag></tag> または <tag> </tag>
text = re.sub(
r'<' + re.escape(tag) + r'\s*>\s*</' + re.escape(tag) + r'>',
f'<{tag}>{value}</{tag}>', text
)
return text
def modify_skeleton_for_post_apply(
file_data_b64: str,
proc_id: str,
proc_name: str,
uketsuke_kikan_id: str,
shinsei_shubetsu: str = "申込",
shimei: str = "テスト タロウ",
shimei_furigana: str = "テスト タロウ",
yubin: str = "1300022",
jusho: str = "東京都墨田区江東橋4丁目31番10号",
jigyosho_seiri_mae: str = "41",
jigyosho_seiri_ato: str = "シスメ",
jigyosho_bangou: str = "23090",
) -> str:
"""
スケルトン ZIP の kousei.xml に必須フィールドを埋めて再パックする。
戻り値: 修正済み ZIP の base64 文字列
"""
raw = base64.b64decode(file_data_b64)
buf_out = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(raw), 'r') as zin:
with zipfile.ZipFile(buf_out, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
name_lower = item.filename.lower()
if name_lower.endswith('/kousei.xml') or name_lower == 'kousei.xml':
data = _modify_kousei_xml(
data, proc_id, proc_name, uketsuke_kikan_id,
shinsei_shubetsu, shimei, shimei_furigana, yubin, jusho
)
elif name_lower.endswith('_01.xml') and 'check' not in name_lower:
# 申請書XML: 事業所情報を埋める
data = _modify_apply_xml(data, jigyosho_seiri_mae, jigyosho_seiri_ato, jigyosho_bangou)
zout.writestr(item, data)
return base64.b64encode(buf_out.getvalue()).decode()
def _modify_apply_xml(xml_bytes: bytes, seiri_mae: str, seiri_ato: str, bangou: str) -> bytes:
"""申請書XML の事業所情報を埋める"""
try:
text = xml_bytes.decode('utf-8')
except Exception:
text = xml_bytes.decode('shift_jis', errors='replace')
text = _fill_empty_tag(text, '事業所整理記号_前', seiri_mae)
text = _fill_empty_tag(text, '事業所整理記号_後', seiri_ato)
text = _fill_empty_tag(text, '事業所番号', bangou)
return text.encode('utf-8')
def _modify_kousei_xml(
xml_bytes: bytes,
proc_id: str,
proc_name: str,
uketsuke_kikan_id: str,
shinsei_shubetsu: str,
shimei: str,
shimei_furigana: str,
yubin: str,
jusho: str,
teishutsusaki_id: str = "900API00000000001001001",
teishutsusaki_name: str = "総務省,行政管理局,API",
jusho_furigana: str = "トウキョウトスミダクコウトウバシ4チョウメ31バン10ゴウ",
tel: str = "03-1234-5678",
email: str = "test@example.com",
) -> bytes:
"""kousei.xml の必須フィールドをテキスト置換で埋める(名前空間非依存)"""
try:
text = xml_bytes.decode('utf-8')
except Exception:
text = xml_bytes.decode('shift_jis', errors='replace')
fills = [
('受付行政機関ID', uketsuke_kikan_id),
('手続ID', proc_id),
('手続名称', proc_name),
('申請種別', shinsei_shubetsu),
('氏名フリガナ', shimei_furigana), # フリガナを先に(氏名より長いので誤置換防止)
('氏名', shimei),
('郵便番号', yubin),
('住所', jusho),
('提出先識別子', teishutsusaki_id),
('提出先名称', teishutsusaki_name),
('住所フリガナ', jusho_furigana),
('電話番号', tel),
('電子メールアドレス', email),
]
for tag, value in fills:
text = _fill_empty_tag(text, tag, value)
# 申請書属性情報を </提出先情報> の直後に挿入(スケルトンに存在しないため)
shinsei_attr = (
'<申請書属性情報>'
'<申請書様式ID>900A13800001000001</申請書様式ID>'
'<申請書様式バージョン>0001</申請書様式バージョン>'
'<申請書様式名称>APIテスト用手続(電子送達関係手続)(通)0001_01</申請書様式名称>'
'<申請書ファイル名称>900A13800001000001_01.xml</申請書ファイル名称>'
'</申請書属性情報>'
)
if '<申請書属性情報>' not in text:
text = text.replace('</提出先情報>', '</提出先情報>' + shinsei_attr, 1)
return text.encode('utf-8')
async def submit_post_apply(access_token: str, proc_id: str,
file_name: str, file_data_b64: str) -> dict:
"""
電子送達利用申込み
POST /post-apply {"proc_id": "...", "send_file": {"file_name": "...", "file_data": "base64..."}}
Returns: {results: {arrive_id, arrive_date, proc_name, ...}}
※電子送達はトライアル非対応のため X-eGovAPI-Trial は送信しない
"""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{API_BASE}/post-apply",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"proc_id": proc_id,
"send_file": {
"file_name": file_name,
"file_data": file_data_b64,
},
},
timeout=120,
)
if not resp.is_success:
try:
err_body = resp.json()
except Exception:
err_body = resp.text
raise RuntimeError(f"HTTP {resp.status_code}: {err_body}")
return resp.json()
async def get_apply_list(access_token: str, limit: int = 100) -> dict:
"""
申請案件一覧取得
GET /apply/lists
Returns: {results: {apply_list: [...]}}
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/apply/lists",
headers={"Authorization": f"Bearer {access_token}"},
params={"limit": limit},
timeout=30,
)
resp.raise_for_status()
return resp.json()
async def get_apply_detail(access_token: str, arrive_id: str) -> dict:
"""
申請案件取得(詳細)
GET /apply/{arrive_id}
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/apply/{arrive_id}",
headers={"Authorization": f"Bearer {access_token}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()