Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5c4f3a4
Add semantic AST helpers for attribute restrictions
yuji38kwmt Apr 28, 2026
5f8d9ca
Use Pydantic model for restriction catalog
yuji38kwmt Apr 28, 2026
697756b
Add schema descriptions to restriction catalog
yuji38kwmt Apr 28, 2026
2ee7d93
属性制約カタログのモデルの説明を日本語に翻訳
yuji38kwmt Apr 28, 2026
0056e4a
属性制約カタログの属性タイプを追加データ定義タイプに変更し、不要な属性タイプを削除
yuji38kwmt Apr 28, 2026
59d1143
新しいSKILL.mdファイルを追加し、Pythonコーディングスタイルに関するガイドラインを記述
yuji38kwmt Apr 28, 2026
c799ae6
config.tomlを新規作成し、承認ポリシーやサンドボックス設定を追加
yuji38kwmt Apr 28, 2026
bafc2d5
copilot-instructions.mdからプロジェクトの目的、開発コマンド、技術スタック、ディレクトリ構造、コーディングスタイル、…
yuji38kwmt Apr 28, 2026
5ca1adf
RestrictionAstのtypeフィールドをstrからRestrictionAstTypeに変更
yuji38kwmt Apr 28, 2026
0042053
Convert RestrictionAst to pydantic model
yuji38kwmt Apr 28, 2026
b77d5a9
Remove redundant RestrictionAst dict helpers
yuji38kwmt Apr 28, 2026
9fa5e6e
SKILL.mdにmatch文の使用を推奨する項目を追加
yuji38kwmt Apr 28, 2026
859e886
Refactor restriction dispatch with match
yuji38kwmt Apr 28, 2026
a470d59
Refactor RestrictionAst human readable conversion
yuji38kwmt Apr 28, 2026
60fe54e
Improve human-readable restriction text
yuji38kwmt Apr 28, 2026
ffd5323
Inline repr in human-readable text
yuji38kwmt Apr 28, 2026
5f6c063
Remove quote_human helper
yuji38kwmt Apr 28, 2026
00d028a
Inline imply human-readable helpers
yuji38kwmt Apr 28, 2026
cfda847
Use assert_noreturn in restriction matches
yuji38kwmt Apr 28, 2026
a911e07
Remove annotation_specs from Restriction.from_dict
yuji38kwmt Apr 28, 2026
ac792cc
SKILL.mdにassert_noreturnの使用に関するガイドラインを追加
yuji38kwmt Apr 28, 2026
c4eccf9
Refactor restriction AST types to enum
yuji38kwmt Apr 28, 2026
1ca35e7
Add docstrings to RestrictionAstType members
yuji38kwmt Apr 28, 2026
97f0543
TypedDictを使用して国際化メッセージの構造を定義し、制約ASTの検証ロジックを改善しました。また、テストコードを更新して新しい型に…
yuji38kwmt Apr 28, 2026
b5100ef
AGENTS.mdにリンターエラーの無視に関する方針を追加し、RestrictionAstクラスのフィールド検証後に型検証を実行するよう修正
yuji38kwmt Apr 28, 2026
c3636a5
Inline RestrictionAst field type validation
yuji38kwmt Apr 28, 2026
9f3a95a
Remove restriction to_python_expr API
yuji38kwmt Apr 28, 2026
b905f23
RestrictionAstクラスのドキュメンテーションから不要な引数説明を削除
yuji38kwmt Apr 28, 2026
730b37c
Inline atomic restriction parsing
yuji38kwmt Apr 28, 2026
1c80feb
Inline simple restriction helpers
yuji38kwmt Apr 28, 2026
4a5aa06
Move AST field rules into RestrictionAst
yuji38kwmt Apr 28, 2026
d2b36a2
Localize allowed AST type helper
yuji38kwmt Apr 28, 2026
031e2f8
Localize human readable AST formatter
yuji38kwmt Apr 28, 2026
8ce3c10
Localize atomic restriction-to-AST conversion
yuji38kwmt Apr 28, 2026
b630da3
Localize equality restriction-to-AST helpers
yuji38kwmt Apr 28, 2026
8dd879b
Localize atomic AST-to-restriction conversion
yuji38kwmt Apr 28, 2026
14d0145
Localize string AST-to-restriction helpers
yuji38kwmt Apr 28, 2026
f53d3a9
Refactor attribute restriction helper inputs
yuji38kwmt Apr 28, 2026
303fc72
Add public attribute factory method
yuji38kwmt Apr 28, 2026
9c803ff
Use create as attribute factory API
yuji38kwmt Apr 28, 2026
3dd9801
Inline empty-check helper in AST conversion
yuji38kwmt Apr 28, 2026
814d57e
Inline attribute choice access
yuji38kwmt Apr 28, 2026
c55afe2
Internalize invalid AST helper
yuji38kwmt Apr 28, 2026
cd3340f
Internalize integer parsing helper
yuji38kwmt Apr 28, 2026
0b4da16
test: pytest.raisesのmatch指定を削除
yuji38kwmt Apr 29, 2026
7ae070a
Remove unnecessary ValueError raise documentation from Restriction class
yuji38kwmt Apr 29, 2026
6328cdb
Optimize link label catalog generation
yuji38kwmt Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .agents/skills/python-coding-style/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: python-coding-style
description: Pythonコードを作成・修正するときに使用。
---

# 全般
* できるだけ型ヒントを付ける。
* できるだけ汎用的な型ヒントをつける。たとえばlistでもsetでも良いならば、`Collection`や`Iterable`を使う。
* 特に理由がない限り、`object`や`Any`は避ける。
* docstring は Google スタイルで記述する。
* ログメッセージやコメントは日本語で記述する
* 戻り値をtupleで返そうとする場合は、`NamedTuple`, `dataclass`, pydantic modelの使用を検討して、本当にtupleが適切かどうかを判断する。
* モジュールレベルの定数、クラス属性、インスタンス属性などには直後に docstring として記述する。VSCodeのtooltipに表示させるため。
* dictから値を取得する際、必須なキーならばブラケット記法を使う。キーが必須がどうか分からない場合は、必須とみなす。
* match文が利用できる箇所では、if文よりもmatch文を使用する。
* 必要ならば `annofabapi.util.type_util.assert_noreturn` を使用して、match文の網羅性を保証する。

25 changes: 25 additions & 0 deletions .agents/skills/test-writing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: test-writing
description: Pythonのテストコードを作成・修正するときに使用。
---

## テスト方針
* `DummyApi`、`DummyService`、`FakeClient` のような自前の疑似実装は原則追加しない。
* `monkeypatch` や `mock` は、外部API、ネットワーク、時刻、環境変数、ファイルシステム外の副作用など、テストで制御しないと不安定になる境界に限定して使用する。
* 関数呼び出しの詳細に強く結びついたテストは、原則として追加しないでください。
* `mock.assert_called_once_with(...)` のような配線確認だけのテストは、明確な回帰防止上の理由がある場合に限って追加してください。
* テストを書くときは「そのテストが将来の設計変更を不必要に妨げないか」を確認してください。
* ただし、外部API連携、ジョブ投入、通知送信、永続化など、副作用の境界にある処理では、呼び出し確認テストを許可します。
* `argparse`などのCLIパーサーで制御している部分はテストを作成しないでください。たとえば必須引数が指定されない場合のエラーなどです。これはライブラリの責務だからです。


## テストの実行方法
* `uv run pytest` を使う。

## テストコードの具体的なスタイル
* テストファイルのディレクトリ構成やファイル名は、`annofabcli/`配下のプロダクションコードと対応させる。
* 例外確認には `pytest.raises` を使用する。
* `pytest.raises` ではエラーメッセージ文字列の確認は行わない。
* 一時ディレクトリが必要な場合は `tmp_path` fixture を使う。
* WebAPI にアクセスするテストには `pytest.mark.access_webapi` を付与する。CLI テストは、引数上はローカル処理に見えても共通ログイン処理を通る場合があるので注意する。

24 changes: 24 additions & 0 deletions .codex/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 承認ポリシー。
# "never" は、Codex がコマンド実行や権限昇格のたびに確認せず進む設定です。
approval_policy = "never"
Comment thread
yuji38kwmt marked this conversation as resolved.

# sandbox の種類。
# "workspace-write" は、workspace 内と許可された追加パスへの書き込みを許可します。
sandbox_mode = "workspace-write"

[shell_environment_policy.set]
# uv のキャッシュ先を /tmp 配下に固定し、ユーザー名依存のパスを避けます。
UV_CACHE_DIR = "/tmp/uv-cache"

[sandbox_workspace_write]
# workspace-write sandbox 内で外向きネットワーク通信を許可します。
network_access = true
Comment thread
yuji38kwmt marked this conversation as resolved.

# workspace に加えて書き込みを許可する追加ディレクトリ一覧です。
# 今回は uv キャッシュと一時ファイル用途を想定して /tmp を許可しています。
writable_roots = [
"/tmp",
]

[features]
codex_hooks = true
39 changes: 1 addition & 38 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,3 @@
# copilot-instructions.md

## プロジェクトの目的
AnnofabのWebAPIのPythonクライアントライブラリです。

## 開発でよく使うコマンド
* コードのフォーマット: `make format`
* Lintの実行: `make lint`
* テストの実行: `make test`
* ドキュメントの実行: `make docs`

## 技術スタック
* Python 3.9 以上
* テストフレームワーク: Pytest v8 以上

## ディレクトリ構造概要

* `annofabcli/**`: アプリケーションのソースコード
* `tests/**`: テストコード
* `tests/data/**`: テストコードが参照するリソース
* `docs/*`: ドキュメント

## コーディングスタイル

### Python
* dictから値を取得する際、必須なキーならばブラケット記法を使う。キーが必須がどうか分からない場合は、必須とみなす。
* できるだけ`os.path`でなく`pathlib.Path`を使う(Lint`flake8-use-pathlib`に従う)
* Noneの判定、空文字列の判定、長さが0のコレクションの判定は、falsyとして判定するのでなく、`if a is not None:`のように判定内容を明記してください。
* 型ヒントを`dict`にする場合、`dict[str, Any]`のようにキーと値の型を指定する。

### テストコード
* Errorの確認は、`pytest.raises`を使用する。エラーメッセージの確認は行わない。
* 一時ディレクトリを使用する場合は、`tmp_path` fixtureを利用する。


## 作業の進め方
* コードの修正が完了したら`make format`を実行してフォーマットを行い、その後`make lint`を実行してLintエラーがないことを確認する。

## レビュー
* PRレビューの際は、日本語でレビューを行う
`AGENTS.md`や`.agents/skills/`以下のファイルを参照してください。
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

## 想定する実行環境
* AIによる指摘や応答は日本語で書く。

## コードの修正方針
* 原則、破壊的変更を行って修正してください。コードをシンプルにするためです。
* 何かを判断する際、コードの修正量は無視してください。AIが修正するので、そこは問題になりません。
* リンターにより行数が長すぎるなどのエラーが発生した場合、必要ならばそのエラーを無視してください。読みやすさやシンプルさを優先してください。

## Coding Agent による作業の進め方
1. コードを修正する。関連するテストコードやドキュメントも修正する。
2. 自分自身でレビューする
3. `make format`, `make lint`を実行する。
4. 関連するテストコードを実行する。
5. Git にコミットする。ただしmainブランチで作業している場合は、pull requestを作成する。
68 changes: 53 additions & 15 deletions annofabapi/util/annotation_specs.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
from typing import Any, Literal
from typing import Any, Literal, TypedDict, cast

import more_itertools

from annofabapi.models import Lang
from annofabapi.pydantic_models.additional_data_definition_type import AdditionalDataDefinitionType


def get_english_message(internationalization_message: dict[str, Any]) -> str:
class InternationalizationMessageItem(TypedDict):
"""多言語メッセージの1要素です。"""

lang: str
message: str


class InternationalizationMessage(TypedDict):
"""多言語メッセージです。"""

messages: list[InternationalizationMessageItem]


class AttributeChoice(TypedDict):
"""属性の選択肢です。"""

choice_id: str
name: InternationalizationMessage


class AttributeDefinition(TypedDict):
"""アノテーション仕様上の属性定義です。"""

additional_data_definition_id: str
name: InternationalizationMessage
type: AdditionalDataDefinitionType
choices: list[AttributeChoice] | None


class LabelDefinition(TypedDict):
"""アノテーション仕様上のラベル定義です。"""

label_id: str
label_name: InternationalizationMessage
additional_data_definitions: list[str]


def get_english_message(internationalization_message: InternationalizationMessage | dict[str, Any]) -> str:
"""
`InternationalizationMessage`クラスの値から、英語メッセージを取得します。
英語メッセージが見つからない場合は ``ValueError`` をスローします。
Expand All @@ -22,7 +60,7 @@ def get_english_message(internationalization_message: dict[str, Any]) -> str:
Raises:
ValueError: 英語メッセージが見つからない場合
"""
messages: list[dict[str, str]] = internationalization_message["messages"]
messages = cast(list[InternationalizationMessageItem], internationalization_message["messages"])
result = more_itertools.first_true(messages, pred=lambda e: e["lang"] == Lang.EN_US.value)
if result is not None:
return result["message"]
Expand All @@ -36,7 +74,7 @@ def get_english_message(internationalization_message: dict[str, Any]) -> str:
"""


def get_message_with_lang(internationalization_message: dict[str, Any], lang: Lang | STR_LANG) -> str | None:
def get_message_with_lang(internationalization_message: InternationalizationMessage | dict[str, Any], lang: Lang | STR_LANG) -> str | None:
"""
`InternationalizationMessage`クラスの値から、指定した ``lang`` に対応するメッセージを取得します。

Expand All @@ -48,7 +86,7 @@ def get_message_with_lang(internationalization_message: dict[str, Any], lang: La
指定した言語に対応するメッセージ。見つからない場合はNoneを返します。

"""
messages: list[dict[str, str]] = internationalization_message["messages"]
messages = cast(list[InternationalizationMessageItem], internationalization_message["messages"])
if isinstance(lang, Lang):
str_lang = lang.value
else:
Expand All @@ -60,7 +98,7 @@ def get_message_with_lang(internationalization_message: dict[str, Any], lang: La
return None


def get_choice(choices: list[dict[str, Any]], *, choice_id: str | None = None, choice_name: str | None = None) -> dict[str, Any]:
def get_choice(choices: list[AttributeChoice], *, choice_id: str | None = None, choice_name: str | None = None) -> AttributeChoice:
"""
選択肢情報を取得します。

Expand Down Expand Up @@ -90,12 +128,12 @@ def get_choice(choices: list[dict[str, Any]], *, choice_id: str | None = None, c


def get_attribute(
additionals: list[dict[str, Any]],
additionals: list[AttributeDefinition],
*,
attribute_id: str | None = None,
attribute_name: str | None = None,
label: dict[str, Any] | None = None,
) -> dict[str, Any]:
label: LabelDefinition | None = None,
) -> AttributeDefinition:
"""
属性情報を取得します。

Expand Down Expand Up @@ -133,7 +171,7 @@ def get_attribute(
return result[0]


def get_label(labels: list[dict[str, Any]], *, label_id: str | None = None, label_name: str | None = None) -> dict[str, Any]:
def get_label(labels: list[LabelDefinition], *, label_id: str | None = None, label_name: str | None = None) -> LabelDefinition:
"""
ラベル情報を取得します。

Expand Down Expand Up @@ -172,12 +210,12 @@ class AnnotationSpecsAccessor:

def __init__(self, annotation_specs: dict[str, Any]) -> None:
self.annotation_specs = annotation_specs
self.labels = annotation_specs["labels"]
self.additionals = annotation_specs["additionals"]
self.labels: list[LabelDefinition] = annotation_specs["labels"]
self.additionals: list[AttributeDefinition] = annotation_specs["additionals"]

def get_attribute(
self, *, attribute_id: str | None = None, attribute_name: str | None = None, label: dict[str, Any] | None = None
) -> dict[str, Any]:
self, *, attribute_id: str | None = None, attribute_name: str | None = None, label: LabelDefinition | None = None
) -> AttributeDefinition:
"""
属性情報を取得します。

Expand All @@ -192,7 +230,7 @@ def get_attribute(
"""
return get_attribute(self.additionals, attribute_id=attribute_id, attribute_name=attribute_name, label=label)

def get_label(self, *, label_id: str | None = None, label_name: str | None = None) -> dict[str, Any]:
def get_label(self, *, label_id: str | None = None, label_name: str | None = None) -> LabelDefinition:
"""
ラベル情報を取得します。

Expand Down
Loading
Loading