signate-deploy 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.
@@ -0,0 +1,27 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Install build tools
21
+ run: pip install build
22
+
23
+ - name: Build package
24
+ run: python -m build
25
+
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install dependencies
24
+ run: pip install -e ".[dev]"
25
+
26
+ - name: Run tests
27
+ run: pytest tests/ -v
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yasunorim
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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: signate-deploy
3
+ Version: 0.1.0
4
+ Summary: A CLI tool to automate SIGNATE competition workflows via GitHub Actions
5
+ Project-URL: Homepage, https://github.com/yasumorishima/signate-deploy
6
+ Project-URL: Repository, https://github.com/yasumorishima/signate-deploy
7
+ Author: yasunorim
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: competition,deploy,github-actions,machine-learning,signate
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: click>=8.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # signate-deploy
29
+
30
+ A CLI tool to automate SIGNATE competition workflows via GitHub Actions.
31
+
32
+ `git push` → GitHub Actions → Download data → Train → Submit to SIGNATE
33
+
34
+ ```
35
+ signate-deploy init-repo # Set up GitHub Actions workflows
36
+ signate-deploy init my-comp \
37
+ --task-key <task_key> \
38
+ --file-key train:<key> \
39
+ --file-key test:<key> # Create competition directory
40
+ signate-deploy submit my-comp \
41
+ --memo "Baseline v1" # Trigger train & submit
42
+ signate-deploy download my-comp # Trigger data download only
43
+ ```
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install signate-deploy
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ### 1. Set up GitHub Actions
54
+
55
+ In your GitHub repository root:
56
+
57
+ ```bash
58
+ signate-deploy init-repo
59
+ ```
60
+
61
+ Creates:
62
+ - `.github/workflows/signate-submit.yml` — full pipeline (download → train → submit)
63
+ - `.github/workflows/signate-download.yml` — data download only
64
+
65
+ ### 2. Set up GitHub Secrets
66
+
67
+ ```bash
68
+ # Generate SIGNATE token
69
+ signate token --email=your@email.com --password=your-password
70
+
71
+ # Set as GitHub Secret (Base64 encoded)
72
+ cat ~/.signate/signate.json | base64 | gh secret set SIGNATE_TOKEN_B64
73
+ ```
74
+
75
+ ### 3. Get task_key and file_keys
76
+
77
+ ```bash
78
+ pip install signate
79
+ signate file-list --task_key <task_key>
80
+ ```
81
+
82
+ `task_key` is in the competition URL:
83
+ ```
84
+ https://user.competition.signate.jp/.../detail/?...&task=THIS_IS_TASK_KEY
85
+ ```
86
+
87
+ ### 4. Create competition directory
88
+
89
+ ```bash
90
+ signate-deploy init my-comp \
91
+ --task-key abc123def456 \
92
+ --file-key train:5f0e1ebb35af4963 \
93
+ --file-key test:72f23ebe8f004fa0 \
94
+ --file-key sample_submit:ad3502af26b9
95
+ ```
96
+
97
+ Creates:
98
+ ```
99
+ my-comp/
100
+ signate-config.json # task_key and file_keys
101
+ train.py # LightGBM 5-fold CV template
102
+ requirements.txt # pandas, numpy, scikit-learn, lightgbm
103
+ ```
104
+
105
+ ### 5. Edit train.py and push
106
+
107
+ ```bash
108
+ # Edit my-comp/train.py (set TARGET column name, add preprocessing, etc.)
109
+ git add my-comp/ && git commit -m "Add my-comp baseline" && git push
110
+ ```
111
+
112
+ ### 6. Submit
113
+
114
+ ```bash
115
+ signate-deploy submit my-comp --memo "Baseline v1"
116
+ # → gh workflow run signate-submit.yml is triggered
117
+
118
+ # Check progress
119
+ gh run list --limit 1
120
+ gh run view --log
121
+ ```
122
+
123
+ ## signate-config.json
124
+
125
+ ```json
126
+ {
127
+ "task_key": "your_task_key",
128
+ "file_keys": {
129
+ "train": "file_key_for_train_csv",
130
+ "test": "file_key_for_test_csv",
131
+ "sample_submit": "file_key_for_sample_submit_csv"
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Notes
137
+
138
+ - `⚠️ Never commit `data/` or `.signate/` — they are .gitignored by `init-repo`
139
+ - Requires [GitHub CLI (`gh`)](https://cli.github.com/) to be installed and authenticated
140
+ - Works on any OS (Windows/Mac/Linux)
141
+
142
+ ## Links
143
+
144
+ - [PyPI](https://pypi.org/project/signate-deploy/)
145
+ - [Article (JP)](https://zenn.dev/shogaku/articles/signate-github-actions-cloud-ml)
146
+ - [Related: kaggle-notebook-deploy](https://github.com/yasumorishima/kaggle-notebook-deploy)
@@ -0,0 +1,119 @@
1
+ # signate-deploy
2
+
3
+ A CLI tool to automate SIGNATE competition workflows via GitHub Actions.
4
+
5
+ `git push` → GitHub Actions → Download data → Train → Submit to SIGNATE
6
+
7
+ ```
8
+ signate-deploy init-repo # Set up GitHub Actions workflows
9
+ signate-deploy init my-comp \
10
+ --task-key <task_key> \
11
+ --file-key train:<key> \
12
+ --file-key test:<key> # Create competition directory
13
+ signate-deploy submit my-comp \
14
+ --memo "Baseline v1" # Trigger train & submit
15
+ signate-deploy download my-comp # Trigger data download only
16
+ ```
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install signate-deploy
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### 1. Set up GitHub Actions
27
+
28
+ In your GitHub repository root:
29
+
30
+ ```bash
31
+ signate-deploy init-repo
32
+ ```
33
+
34
+ Creates:
35
+ - `.github/workflows/signate-submit.yml` — full pipeline (download → train → submit)
36
+ - `.github/workflows/signate-download.yml` — data download only
37
+
38
+ ### 2. Set up GitHub Secrets
39
+
40
+ ```bash
41
+ # Generate SIGNATE token
42
+ signate token --email=your@email.com --password=your-password
43
+
44
+ # Set as GitHub Secret (Base64 encoded)
45
+ cat ~/.signate/signate.json | base64 | gh secret set SIGNATE_TOKEN_B64
46
+ ```
47
+
48
+ ### 3. Get task_key and file_keys
49
+
50
+ ```bash
51
+ pip install signate
52
+ signate file-list --task_key <task_key>
53
+ ```
54
+
55
+ `task_key` is in the competition URL:
56
+ ```
57
+ https://user.competition.signate.jp/.../detail/?...&task=THIS_IS_TASK_KEY
58
+ ```
59
+
60
+ ### 4. Create competition directory
61
+
62
+ ```bash
63
+ signate-deploy init my-comp \
64
+ --task-key abc123def456 \
65
+ --file-key train:5f0e1ebb35af4963 \
66
+ --file-key test:72f23ebe8f004fa0 \
67
+ --file-key sample_submit:ad3502af26b9
68
+ ```
69
+
70
+ Creates:
71
+ ```
72
+ my-comp/
73
+ signate-config.json # task_key and file_keys
74
+ train.py # LightGBM 5-fold CV template
75
+ requirements.txt # pandas, numpy, scikit-learn, lightgbm
76
+ ```
77
+
78
+ ### 5. Edit train.py and push
79
+
80
+ ```bash
81
+ # Edit my-comp/train.py (set TARGET column name, add preprocessing, etc.)
82
+ git add my-comp/ && git commit -m "Add my-comp baseline" && git push
83
+ ```
84
+
85
+ ### 6. Submit
86
+
87
+ ```bash
88
+ signate-deploy submit my-comp --memo "Baseline v1"
89
+ # → gh workflow run signate-submit.yml is triggered
90
+
91
+ # Check progress
92
+ gh run list --limit 1
93
+ gh run view --log
94
+ ```
95
+
96
+ ## signate-config.json
97
+
98
+ ```json
99
+ {
100
+ "task_key": "your_task_key",
101
+ "file_keys": {
102
+ "train": "file_key_for_train_csv",
103
+ "test": "file_key_for_test_csv",
104
+ "sample_submit": "file_key_for_sample_submit_csv"
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## Notes
110
+
111
+ - `⚠️ Never commit `data/` or `.signate/` — they are .gitignored by `init-repo`
112
+ - Requires [GitHub CLI (`gh`)](https://cli.github.com/) to be installed and authenticated
113
+ - Works on any OS (Windows/Mac/Linux)
114
+
115
+ ## Links
116
+
117
+ - [PyPI](https://pypi.org/project/signate-deploy/)
118
+ - [Article (JP)](https://zenn.dev/shogaku/articles/signate-github-actions-cloud-ml)
119
+ - [Related: kaggle-notebook-deploy](https://github.com/yasumorishima/kaggle-notebook-deploy)
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "signate-deploy"
7
+ version = "0.1.0"
8
+ description = "A CLI tool to automate SIGNATE competition workflows via GitHub Actions"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "yasunorim" },
14
+ ]
15
+ keywords = ["signate", "deploy", "github-actions", "machine-learning", "competition"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ ]
28
+ dependencies = [
29
+ "click>=8.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0",
35
+ "pytest-cov>=4.0",
36
+ ]
37
+
38
+ [project.scripts]
39
+ signate-deploy = "signate_deploy.cli:main"
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/yasumorishima/signate-deploy"
43
+ Repository = "https://github.com/yasumorishima/signate-deploy"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/signate_deploy"]
@@ -0,0 +1,3 @@
1
+ """signate-deploy: GitHub Actions経由でSIGNATEコンペを自動化するCLIツール"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,25 @@
1
+ """CLI entry point for signate-deploy."""
2
+
3
+ import click
4
+
5
+ from signate_deploy import __version__
6
+ from signate_deploy.commands.init_repo import init_repo
7
+ from signate_deploy.commands.init import init
8
+ from signate_deploy.commands.submit import submit
9
+ from signate_deploy.commands.download import download
10
+
11
+
12
+ @click.group()
13
+ @click.version_option(version=__version__)
14
+ def main():
15
+ """GitHub Actions経由でSIGNATEコンペを自動化するCLIツール
16
+
17
+ データDL → 学習 → 提出 の一気通貫パイプラインをセットアップします。
18
+ """
19
+ pass
20
+
21
+
22
+ main.add_command(init_repo)
23
+ main.add_command(init)
24
+ main.add_command(submit)
25
+ main.add_command(download)
@@ -0,0 +1,45 @@
1
+ """signate-deploy download: GitHub Actions経由でデータをダウンロードする."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+
9
+ @click.command("download")
10
+ @click.argument("competition_dir")
11
+ def download(competition_dir):
12
+ """GitHub Actions経由でSIGNATEからデータをダウンロードする.
13
+
14
+ COMPETITION_DIR 内の signate-config.json を使い、
15
+ GitHub Actions の signate-download.yml を起動します。
16
+
17
+ 例:
18
+ signate-deploy download my-comp
19
+ """
20
+ config_path = Path(competition_dir) / "signate-config.json"
21
+ if not config_path.exists():
22
+ click.echo(f"Error: {config_path} が見つかりません。", err=True)
23
+ click.echo("signate-deploy init でディレクトリを作成してください。", err=True)
24
+ raise SystemExit(1)
25
+
26
+ click.echo(f"Triggering download workflow for '{competition_dir}'...")
27
+
28
+ result = subprocess.run(
29
+ [
30
+ "gh", "workflow", "run", "signate-download.yml",
31
+ "-f", f"competition_dir={competition_dir}",
32
+ ],
33
+ capture_output=True,
34
+ text=True,
35
+ )
36
+
37
+ if result.returncode != 0:
38
+ click.echo("Error: gh workflow run に失敗しました。", err=True)
39
+ click.echo(result.stderr, err=True)
40
+ raise SystemExit(1)
41
+
42
+ click.echo("")
43
+ click.echo("Workflow を起動しました。")
44
+ click.echo("進捗確認: gh run list --limit 1")
45
+ click.echo("Artifact取得: gh run download <run_id> --dir data/")
@@ -0,0 +1,139 @@
1
+ """signate-deploy init: コンペ用ディレクトリを雛形から生成する."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+
9
+ CONFIG_TEMPLATE = {
10
+ "task_key": "",
11
+ "file_keys": {},
12
+ }
13
+
14
+ TRAIN_TEMPLATE = """\
15
+ import pandas as pd
16
+ import numpy as np
17
+ from sklearn.model_selection import StratifiedKFold
18
+ from sklearn.metrics import roc_auc_score
19
+ import lightgbm as lgb
20
+
21
+ DATA_DIR = "{competition_dir}/data"
22
+ TARGET = "target" # ターゲット列名に変更してください
23
+
24
+
25
+ def main():
26
+ train = pd.read_csv(f"{{DATA_DIR}}/train.csv")
27
+ test = pd.read_csv(f"{{DATA_DIR}}/test.csv")
28
+
29
+ features = [c for c in train.columns if c not in ["id", TARGET]]
30
+ X, y = train[features], train[TARGET]
31
+ X_test = test[features]
32
+
33
+ params = {{
34
+ "objective": "binary",
35
+ "metric": "auc",
36
+ "verbosity": -1,
37
+ "n_estimators": 1000,
38
+ "learning_rate": 0.05,
39
+ "random_state": 42,
40
+ }}
41
+
42
+ skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
43
+ oof_preds = np.zeros(len(X))
44
+ test_preds = np.zeros(len(X_test))
45
+
46
+ for fold, (tr_idx, val_idx) in enumerate(skf.split(X, y)):
47
+ model = lgb.LGBMClassifier(**params)
48
+ model.fit(
49
+ X.iloc[tr_idx], y.iloc[tr_idx],
50
+ eval_set=[(X.iloc[val_idx], y.iloc[val_idx])],
51
+ callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)],
52
+ )
53
+ oof_preds[val_idx] = model.predict_proba(X.iloc[val_idx])[:, 1]
54
+ test_preds += model.predict_proba(X_test)[:, 1] / 5
55
+ print(f"Fold {{fold + 1}}: AUC = {{roc_auc_score(y.iloc[val_idx], oof_preds[val_idx]):.5f}}")
56
+
57
+ print(f"Overall OOF AUC: {{roc_auc_score(y, oof_preds):.5f}}")
58
+
59
+ submission = pd.DataFrame({{"id": test["id"], "pred": test_preds}})
60
+ submission.to_csv(f"{{DATA_DIR}}/../submission.csv", index=False, header=False)
61
+ print(f"Saved: submission.csv ({{len(submission)}} rows)")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
66
+ """
67
+
68
+ REQUIREMENTS_TEMPLATE = """\
69
+ pandas
70
+ numpy
71
+ scikit-learn
72
+ lightgbm
73
+ """
74
+
75
+
76
+ @click.command("init")
77
+ @click.argument("competition_dir")
78
+ @click.option("--task-key", required=True, help="SIGNATEのtask_key(コンペURLから取得)")
79
+ @click.option(
80
+ "--file-key",
81
+ multiple=True,
82
+ metavar="NAME:KEY",
83
+ help="ファイルキー(例: --file-key train:abc123 --file-key test:def456)",
84
+ )
85
+ def init(competition_dir, task_key, file_key):
86
+ """コンペ用ディレクトリを雛形から生成する.
87
+
88
+ COMPETITION_DIR はローカルのディレクトリ名です。
89
+
90
+ 例:
91
+ signate-deploy init my-comp --task-key abc123
92
+ signate-deploy init my-comp --task-key abc123 --file-key train:key1 --file-key test:key2
93
+ """
94
+ dir_path = Path(competition_dir)
95
+ if dir_path.exists():
96
+ click.echo(f"Error: ディレクトリ '{competition_dir}' は既に存在します。", err=True)
97
+ raise SystemExit(1)
98
+
99
+ dir_path.mkdir(parents=True)
100
+
101
+ # file_keys をパース
102
+ file_keys = {}
103
+ for fk in file_key:
104
+ if ":" not in fk:
105
+ click.echo(f"Error: --file-key の形式が不正です(NAME:KEY 形式で指定してください): {fk}", err=True)
106
+ raise SystemExit(1)
107
+ name, key = fk.split(":", 1)
108
+ file_keys[name] = key
109
+
110
+ # signate-config.json
111
+ config = CONFIG_TEMPLATE.copy()
112
+ config["task_key"] = task_key
113
+ config["file_keys"] = file_keys
114
+
115
+ config_path = dir_path / "signate-config.json"
116
+ with open(config_path, "w") as f:
117
+ json.dump(config, f, indent=2, ensure_ascii=False)
118
+ f.write("\n")
119
+ click.echo(f" Created: {config_path}")
120
+
121
+ # train.py
122
+ train_path = dir_path / "train.py"
123
+ train_path.write_text(TRAIN_TEMPLATE.format(competition_dir=competition_dir))
124
+ click.echo(f" Created: {train_path}")
125
+
126
+ # requirements.txt
127
+ req_path = dir_path / "requirements.txt"
128
+ req_path.write_text(REQUIREMENTS_TEMPLATE)
129
+ click.echo(f" Created: {req_path}")
130
+
131
+ click.echo("")
132
+ click.echo(f"'{competition_dir}/' を作成しました。")
133
+ click.echo("")
134
+ click.echo("次のステップ:")
135
+ click.echo(f" 1. {train_path} を編集(TARGET列名等を変更)")
136
+ click.echo(f" 2. {config_path} のfile_keysを設定(まだなら)")
137
+ click.echo(f" 3. git add {competition_dir}/ && git commit && git push")
138
+ click.echo(f" 4. signate-deploy download {competition_dir} # データDL確認")
139
+ click.echo(f" 5. signate-deploy submit {competition_dir} --memo 'Baseline v1'")
@@ -0,0 +1,213 @@
1
+ """signate-deploy init-repo: リポジトリにGitHub Actionsワークフローをセットアップする."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+
8
+ SUBMIT_WORKFLOW = """\
9
+ name: SIGNATE Train & Submit
10
+
11
+ on:
12
+ workflow_dispatch:
13
+ inputs:
14
+ competition_dir:
15
+ description: "Competition directory name"
16
+ required: true
17
+ type: string
18
+ memo:
19
+ description: "Submission memo"
20
+ required: false
21
+ default: "GitHub Actions submission"
22
+
23
+ jobs:
24
+ submit:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - name: Setup SIGNATE token
34
+ run: |
35
+ mkdir -p ~/.signate
36
+ echo '${{ secrets.SIGNATE_TOKEN_B64 }}' | base64 -d > ~/.signate/signate.json
37
+
38
+ - name: Install dependencies
39
+ run: |
40
+ pip install signate
41
+ if [ -f "${{ inputs.competition_dir }}/requirements.txt" ]; then
42
+ pip install -r "${{ inputs.competition_dir }}/requirements.txt"
43
+ else
44
+ pip install pandas numpy scikit-learn lightgbm
45
+ fi
46
+
47
+ - name: Download data
48
+ run: |
49
+ python - <<'EOF'
50
+ import json, subprocess, os
51
+ config = json.load(open("${{ inputs.competition_dir }}/signate-config.json"))
52
+ task_key = config["task_key"]
53
+ os.makedirs("${{ inputs.competition_dir }}/data", exist_ok=True)
54
+ os.chdir("${{ inputs.competition_dir }}/data")
55
+ for file_key in config.get("file_keys", {}).values():
56
+ subprocess.run(
57
+ ["signate", "download", "--task_key", task_key, "--file_key", file_key],
58
+ check=True,
59
+ )
60
+ EOF
61
+
62
+ - name: Train and predict
63
+ run: python "${{ inputs.competition_dir }}/train.py"
64
+
65
+ - name: Submit
66
+ run: |
67
+ python - <<'EOF'
68
+ import json, subprocess
69
+ config = json.load(open("${{ inputs.competition_dir }}/signate-config.json"))
70
+ subprocess.run([
71
+ "signate", "submit",
72
+ "${{ inputs.competition_dir }}/submission.csv",
73
+ "--task_key", config["task_key"],
74
+ "--memo", "${{ inputs.memo }}",
75
+ ], check=True)
76
+ EOF
77
+
78
+ - name: Upload submission as artifact
79
+ uses: actions/upload-artifact@v4
80
+ with:
81
+ name: submission-${{ github.run_number }}
82
+ path: ${{ inputs.competition_dir }}/submission.csv
83
+ retention-days: 90
84
+ """
85
+
86
+ DOWNLOAD_WORKFLOW = """\
87
+ name: SIGNATE Download Data
88
+
89
+ on:
90
+ workflow_dispatch:
91
+ inputs:
92
+ competition_dir:
93
+ description: "Competition directory name"
94
+ required: true
95
+ type: string
96
+
97
+ jobs:
98
+ download:
99
+ runs-on: ubuntu-latest
100
+ steps:
101
+ - uses: actions/checkout@v4
102
+
103
+ - uses: actions/setup-python@v5
104
+ with:
105
+ python-version: "3.12"
106
+
107
+ - name: Setup SIGNATE token
108
+ run: |
109
+ mkdir -p ~/.signate
110
+ echo '${{ secrets.SIGNATE_TOKEN_B64 }}' | base64 -d > ~/.signate/signate.json
111
+
112
+ - name: Install signate
113
+ run: pip install signate
114
+
115
+ - name: Download data
116
+ run: |
117
+ python - <<'EOF'
118
+ import json, subprocess, os
119
+ config = json.load(open("${{ inputs.competition_dir }}/signate-config.json"))
120
+ task_key = config["task_key"]
121
+ os.makedirs("${{ inputs.competition_dir }}/data", exist_ok=True)
122
+ os.chdir("${{ inputs.competition_dir }}/data")
123
+ for name, file_key in config.get("file_keys", {}).items():
124
+ print(f"Downloading {name}...")
125
+ subprocess.run(
126
+ ["signate", "download", "--task_key", task_key, "--file_key", file_key],
127
+ check=True,
128
+ )
129
+ EOF
130
+
131
+ - name: Upload data as artifact
132
+ uses: actions/upload-artifact@v4
133
+ with:
134
+ name: signate-data-${{ inputs.competition_dir }}
135
+ path: ${{ inputs.competition_dir }}/data/
136
+ retention-days: 90
137
+ """
138
+
139
+ GITIGNORE_ADDITIONS = """\
140
+ # === signate-deploy ===
141
+ # Data files
142
+ data/
143
+ *.csv
144
+ *.zip
145
+
146
+ # Credentials (NEVER commit these)
147
+ .signate/
148
+ signate.json
149
+
150
+ # Virtual environments
151
+ .venv/
152
+ venv/
153
+ """
154
+
155
+
156
+ @click.command("init-repo")
157
+ @click.option("--force", "-f", is_flag=True, default=False, help="既存ファイルを上書きする")
158
+ def init_repo(force):
159
+ """リポジトリにGitHub Actionsワークフローと.gitignoreをセットアップする.
160
+
161
+ カレントディレクトリに以下を生成します:
162
+ - .github/workflows/signate-submit.yml
163
+ - .github/workflows/signate-download.yml
164
+ - .gitignore への追記
165
+ """
166
+ created = []
167
+
168
+ workflow_dir = Path(".github/workflows")
169
+ workflow_dir.mkdir(parents=True, exist_ok=True)
170
+
171
+ for filename, content in [
172
+ ("signate-submit.yml", SUBMIT_WORKFLOW),
173
+ ("signate-download.yml", DOWNLOAD_WORKFLOW),
174
+ ]:
175
+ path = workflow_dir / filename
176
+ if path.exists() and not force:
177
+ click.echo(f" Skip: {path} (既に存在。--force で上書き)")
178
+ else:
179
+ path.write_text(content)
180
+ created.append(str(path))
181
+ click.echo(f" Created: {path}")
182
+
183
+ # .gitignore
184
+ gitignore_path = Path(".gitignore")
185
+ marker = "# === signate-deploy ==="
186
+ if gitignore_path.exists():
187
+ existing = gitignore_path.read_text()
188
+ if marker in existing and not force:
189
+ click.echo(f" Skip: {gitignore_path} (signate-deployセクション追加済み)")
190
+ else:
191
+ if marker not in existing:
192
+ with open(gitignore_path, "a") as f:
193
+ f.write("\n" + GITIGNORE_ADDITIONS)
194
+ created.append(f"{gitignore_path} (追記)")
195
+ click.echo(f" Updated: {gitignore_path}")
196
+ else:
197
+ gitignore_path.write_text(GITIGNORE_ADDITIONS)
198
+ created.append(str(gitignore_path))
199
+ click.echo(f" Created: {gitignore_path}")
200
+
201
+ click.echo("")
202
+ if created:
203
+ click.echo(f"{len(created)}個のファイルをセットアップしました。")
204
+ else:
205
+ click.echo("全てのファイルが既に存在しています。")
206
+
207
+ click.echo("")
208
+ click.echo("次のステップ:")
209
+ click.echo(" 1. GitHub Secretsを設定:")
210
+ click.echo(" signate token --email=your@email.com --password=your-password")
211
+ click.echo(" cat ~/.signate/signate.json | base64 | gh secret set SIGNATE_TOKEN_B64")
212
+ click.echo(" 2. コンペ用ディレクトリを作成:")
213
+ click.echo(" signate-deploy init <competition-dir> --task-key <task_key>")
@@ -0,0 +1,49 @@
1
+ """signate-deploy submit: GitHub Actions経由でSIGNATEに提出する."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+
9
+ @click.command("submit")
10
+ @click.argument("competition_dir")
11
+ @click.option("--memo", "-m", default="GitHub Actions submission", help="提出メモ")
12
+ def submit(competition_dir, memo):
13
+ """GitHub Actions経由でSIGNATEに提出する.
14
+
15
+ COMPETITION_DIR 内の signate-config.json を使い、
16
+ GitHub Actions の signate-submit.yml を起動します。
17
+
18
+ 例:
19
+ signate-deploy submit my-comp
20
+ signate-deploy submit my-comp --memo "LightGBM baseline v1"
21
+ """
22
+ config_path = Path(competition_dir) / "signate-config.json"
23
+ if not config_path.exists():
24
+ click.echo(f"Error: {config_path} が見つかりません。", err=True)
25
+ click.echo("signate-deploy init でディレクトリを作成してください。", err=True)
26
+ raise SystemExit(1)
27
+
28
+ click.echo(f"Triggering submit workflow for '{competition_dir}'...")
29
+ click.echo(f" Memo: {memo}")
30
+
31
+ result = subprocess.run(
32
+ [
33
+ "gh", "workflow", "run", "signate-submit.yml",
34
+ "-f", f"competition_dir={competition_dir}",
35
+ "-f", f"memo={memo}",
36
+ ],
37
+ capture_output=True,
38
+ text=True,
39
+ )
40
+
41
+ if result.returncode != 0:
42
+ click.echo("Error: gh workflow run に失敗しました。", err=True)
43
+ click.echo(result.stderr, err=True)
44
+ raise SystemExit(1)
45
+
46
+ click.echo("")
47
+ click.echo("Workflow を起動しました。")
48
+ click.echo("進捗確認: gh run list --limit 1")
49
+ click.echo("ログ確認: gh run view --log")
File without changes
@@ -0,0 +1,69 @@
1
+ """Tests for init command."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+ from click.testing import CliRunner
7
+ from signate_deploy.cli import main
8
+
9
+
10
+ def test_init_creates_directory(tmp_path, monkeypatch):
11
+ monkeypatch.chdir(tmp_path)
12
+ runner = CliRunner()
13
+ result = runner.invoke(main, ["init", "my-comp", "--task-key", "abc123"])
14
+ assert result.exit_code == 0
15
+ assert (tmp_path / "my-comp").is_dir()
16
+
17
+
18
+ def test_init_creates_config(tmp_path, monkeypatch):
19
+ monkeypatch.chdir(tmp_path)
20
+ runner = CliRunner()
21
+ result = runner.invoke(main, [
22
+ "init", "my-comp",
23
+ "--task-key", "abc123",
24
+ "--file-key", "train:key1",
25
+ "--file-key", "test:key2",
26
+ ])
27
+ assert result.exit_code == 0
28
+ config = json.loads((tmp_path / "my-comp" / "signate-config.json").read_text())
29
+ assert config["task_key"] == "abc123"
30
+ assert config["file_keys"]["train"] == "key1"
31
+ assert config["file_keys"]["test"] == "key2"
32
+
33
+
34
+ def test_init_creates_train_py(tmp_path, monkeypatch):
35
+ monkeypatch.chdir(tmp_path)
36
+ runner = CliRunner()
37
+ result = runner.invoke(main, ["init", "my-comp", "--task-key", "abc123"])
38
+ assert result.exit_code == 0
39
+ assert (tmp_path / "my-comp" / "train.py").exists()
40
+
41
+
42
+ def test_init_creates_requirements(tmp_path, monkeypatch):
43
+ monkeypatch.chdir(tmp_path)
44
+ runner = CliRunner()
45
+ result = runner.invoke(main, ["init", "my-comp", "--task-key", "abc123"])
46
+ assert result.exit_code == 0
47
+ assert (tmp_path / "my-comp" / "requirements.txt").exists()
48
+
49
+
50
+ def test_init_fails_if_dir_exists(tmp_path, monkeypatch):
51
+ monkeypatch.chdir(tmp_path)
52
+ (tmp_path / "my-comp").mkdir()
53
+ runner = CliRunner()
54
+ result = runner.invoke(main, ["init", "my-comp", "--task-key", "abc123"])
55
+ assert result.exit_code != 0
56
+
57
+
58
+ def test_init_invalid_file_key_format(tmp_path, monkeypatch):
59
+ monkeypatch.chdir(tmp_path)
60
+ runner = CliRunner()
61
+ result = runner.invoke(main, ["init", "my-comp", "--task-key", "abc123", "--file-key", "invalid"])
62
+ assert result.exit_code != 0
63
+
64
+
65
+ def test_init_requires_task_key(tmp_path, monkeypatch):
66
+ monkeypatch.chdir(tmp_path)
67
+ runner = CliRunner()
68
+ result = runner.invoke(main, ["init", "my-comp"])
69
+ assert result.exit_code != 0
@@ -0,0 +1,53 @@
1
+ """Tests for init-repo command."""
2
+
3
+ import pytest
4
+ from click.testing import CliRunner
5
+ from signate_deploy.cli import main
6
+
7
+
8
+ def test_init_repo_creates_workflows(tmp_path, monkeypatch):
9
+ monkeypatch.chdir(tmp_path)
10
+ runner = CliRunner()
11
+ result = runner.invoke(main, ["init-repo"])
12
+ assert result.exit_code == 0
13
+ assert (tmp_path / ".github" / "workflows" / "signate-submit.yml").exists()
14
+ assert (tmp_path / ".github" / "workflows" / "signate-download.yml").exists()
15
+
16
+
17
+ def test_init_repo_creates_gitignore(tmp_path, monkeypatch):
18
+ monkeypatch.chdir(tmp_path)
19
+ runner = CliRunner()
20
+ result = runner.invoke(main, ["init-repo"])
21
+ assert result.exit_code == 0
22
+ assert (tmp_path / ".gitignore").exists()
23
+ content = (tmp_path / ".gitignore").read_text()
24
+ assert "signate-deploy" in content
25
+
26
+
27
+ def test_init_repo_appends_to_existing_gitignore(tmp_path, monkeypatch):
28
+ monkeypatch.chdir(tmp_path)
29
+ (tmp_path / ".gitignore").write_text("*.pyc\n")
30
+ runner = CliRunner()
31
+ result = runner.invoke(main, ["init-repo"])
32
+ assert result.exit_code == 0
33
+ content = (tmp_path / ".gitignore").read_text()
34
+ assert "*.pyc" in content
35
+ assert "signate-deploy" in content
36
+
37
+
38
+ def test_init_repo_skip_existing(tmp_path, monkeypatch):
39
+ monkeypatch.chdir(tmp_path)
40
+ runner = CliRunner()
41
+ runner.invoke(main, ["init-repo"])
42
+ result = runner.invoke(main, ["init-repo"])
43
+ assert result.exit_code == 0
44
+ assert "Skip" in result.output
45
+
46
+
47
+ def test_init_repo_force_overwrite(tmp_path, monkeypatch):
48
+ monkeypatch.chdir(tmp_path)
49
+ runner = CliRunner()
50
+ runner.invoke(main, ["init-repo"])
51
+ result = runner.invoke(main, ["init-repo", "--force"])
52
+ assert result.exit_code == 0
53
+ assert "Skip" not in result.output