diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..76edfa6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1 @@ +{"image":"mcr.microsoft.com/devcontainers/universal:2"} \ No newline at end of file diff --git a/.github/PR_NOTES/PR-134-note.md b/.github/PR_NOTES/PR-134-note.md new file mode 100644 index 0000000..9e1b079 --- /dev/null +++ b/.github/PR_NOTES/PR-134-note.md @@ -0,0 +1,10 @@ +PR #134 update +================ + +Moved `SUBAGENT.md` from `.vscode` to `.github/skills/summarize-repo-structure/SUBAGENT.md` and removed the `.vscode` copy. + +Test results (local): 162 passed, 70 warnings. + +Summarizer: output saved to `/tmp/repo_summary_local.json`. + +This file was added automatically by the assistant to surface the update in the pull request. diff --git a/.github/scripts/merge_pr.sh b/.github/scripts/merge_pr.sh new file mode 100644 index 0000000..dbac9e3 --- /dev/null +++ b/.github/scripts/merge_pr.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./merge_pr.sh [repo] [mode] +# mode: squash (default) or merge +PR_NUMBER=${1:-134} +REPO=${2:-coinbase/coinbase-advanced-py} +MODE=${3:-squash} + +if ! command -v gh >/dev/null 2>&1; then + echo "gh CLI is required. Install and authenticate first (gh auth login)." + exit 2 +fi + +echo "Checking gh auth..." +if ! gh auth status >/dev/null 2>&1; then + echo "Not authenticated with GitHub CLI. Run: gh auth login" >&2 + exit 3 +fi + +echo "Merging PR #${PR_NUMBER} on ${REPO} (mode=${MODE})" +# Use the global --repo flag before the subcommand to avoid positional-arg parsing +if [ "${MODE}" = "squash" ]; then + gh --repo "${REPO}" pr merge "${PR_NUMBER}" --squash --delete-branch -m "Merge repo summarizer skill: CI, docs, and subagent metadata" +else + gh --repo "${REPO}" pr merge "${PR_NUMBER}" --merge -m "Merge repo summarizer skill: CI, docs, and subagent metadata" +fi + +echo "Done." diff --git a/.github/skills/summarize-repo-structure/SUBAGENT.md b/.github/skills/summarize-repo-structure/SUBAGENT.md new file mode 100644 index 0000000..45e64d2 --- /dev/null +++ b/.github/skills/summarize-repo-structure/SUBAGENT.md @@ -0,0 +1,21 @@ +--- +name: summarize-repo-structure-subagent +description: Run the repository summarizer to produce a concise JSON summary of the workspace layout. +entrypoint: tools/skills/summarize_repo.py +runner: .vscode/skills/run_summarize_repo.sh +outputs: + - repo_summary.json +invocation: + manual: bash .vscode/skills/run_summarize_repo.sh + ci_workflow: .github/workflows/summarize_repo.yml +author: automated by GitHub Copilot assistant +--- + +This file registers a local "subagent" descriptor for the `summarize-repo-structure` skill. +Tools or automation that look for subagent manifests can use this file to discover the +entrypoint, runner script, and outputs. + +Notes +----- +- The runner script is executable and calls the Python entrypoint. It prints pretty JSON when run. +- The GitHub Actions workflow already produces `repo_summary.json` and uploads it as an artifact. diff --git a/.github/workflows/summarize_repo.yml b/.github/workflows/summarize_repo.yml new file mode 100644 index 0000000..544d0d4 --- /dev/null +++ b/.github/workflows/summarize_repo.yml @@ -0,0 +1,33 @@ +name: Repo Summarizer + +on: + push: + branches: + - master + workflow_dispatch: {} + +jobs: + summarize: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install minimal deps + run: | + python -m pip install --upgrade pip + + - name: Run summarizer + run: | + python3 ./.vscode/skills/../tools/skills/summarize_repo.py --workspace-root . --depth 2 --pretty > repo_summary.json + + - name: Upload summary + uses: actions/upload-artifact@v4 + with: + name: repo-summary + path: repo_summary.json diff --git a/README.md b/README.md index 94fe83d..3d54dad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![PyPI version](https://badge.fury.io/py/coinbase-advanced-py.svg)](https://badge.fury.io/py/coinbase-advanced-py) [![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/license/apache-2-0/) [![Code Style](https://img.shields.io/badge/code_style-black-black)](https://black.readthedocs.io/en/stable/) +[![Repo Summarizer](https://github.com/coinbase/coinbase-advanced-py/actions/workflows/summarize_repo.yml/badge.svg?branch=master)](https://github.com/coinbase/coinbase-advanced-py/actions/workflows/summarize_repo.yml) Welcome to the official Coinbase Advanced API Python SDK. This python project was created to allow coders to easily plug into the [Coinbase Advanced API](https://docs.cdp.coinbase.com/advanced-trade/docs/welcome). This SDK also supports easy connection to the [Coinbase Advanced Trade WebSocket API](https://docs.cdp.coinbase.com/advanced-trade/docs/ws-overview). diff --git a/tools/skills/summarize_repo.py b/tools/skills/summarize_repo.py new file mode 100644 index 0000000..5c2ffae --- /dev/null +++ b/tools/skills/summarize_repo.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Summarize repository structure as JSON. + +Usage: python tools/skills/summarize_repo.py --workspace-root . --depth 2 +""" +from __future__ import annotations + +import argparse +import ast +import json +import os +import re +import sys + +try: + import tomllib as toml +except Exception: + toml = None + + +def read_pyproject(path: str) -> dict | None: + if not toml: + return None + try: + with open(path, "rb") as f: + return toml.load(f) + except Exception: + return None + + +def read_setup_name(path: str) -> str | None: + try: + text = open(path, "r", encoding="utf-8").read() + except Exception: + return None + # Try quick regex for name='pkg' + m = re.search(r"name\s*=\s*['\"]([^'\"]+)['\"]", text) + if m: + return m.group(1) + # Try to AST-parse and find setup(...) call with keyword 'name' + try: + tree = ast.parse(text) + for node in ast.walk(tree): + if isinstance(node, ast.Call) and getattr(node.func, 'id', '') == 'setup': + for kw in node.keywords: + if kw.arg == 'name' and isinstance(kw.value, ast.Constant): + return kw.value.value + except Exception: + pass + return None + + +def list_packages(root: str, depth: int) -> list: + out = [] + for entry in sorted(os.listdir(root)): + p = os.path.join(root, entry) + if os.path.isdir(p) and os.path.exists(os.path.join(p, "__init__.py")): + pkg = {"name": entry, "subpackages": []} + if depth > 1: + try: + for sub in sorted(os.listdir(p)): + sp = os.path.join(p, sub) + if os.path.isdir(sp) and os.path.exists(os.path.join(sp, "__init__.py")): + pkg["subpackages"].append(sub) + except Exception: + pass + out.append(pkg) + return out + + +def top_level_modules(root: str) -> list: + mods = [] + for f in sorted(os.listdir(root)): + if f.endswith('.py') and f != '__init__.py': + mods.append(f) + return mods + + +def collect_tests(root: str) -> list: + tests_dir = os.path.join(root, 'tests') + if not os.path.isdir(tests_dir): + tests_dir = os.path.join(root, 'test') + if not os.path.isdir(tests_dir): + return [] + files = [] + for dirpath, dirnames, filenames in os.walk(tests_dir): + for fn in filenames: + if fn.endswith('.py'): + rel = os.path.relpath(os.path.join(dirpath, fn), root) + files.append(rel) + return sorted(files) + + +def notable_files(root: str) -> list: + candidates = ['README.md', 'README.rst', 'LICENSE', 'LICENSE.md', 'setup.py', 'pyproject.toml'] + return [c for c in candidates if os.path.exists(os.path.join(root, c))] + + +def summarize(root: str, depth: int) -> dict: + root = os.path.abspath(root) + pkg_name = None + pyproject = os.path.join(root, 'pyproject.toml') + if os.path.exists(pyproject): + data = read_pyproject(pyproject) + if data: + # Try common locations for project name + name = data.get('project', {}).get('name') if isinstance(data.get('project'), dict) else None + if not name: + name = data.get('tool', {}).get('poetry', {}).get('name') if isinstance(data.get('tool'), dict) else None + pkg_name = name + setup_py = os.path.join(root, 'setup.py') + if not pkg_name and os.path.exists(setup_py): + pkg_name = read_setup_name(setup_py) + + packages = list_packages(root, depth) + modules = top_level_modules(root) + tests = collect_tests(root) + notes = notable_files(root) + + summary = f"Repository at {os.path.basename(root)}: {len(packages)} package(s), {len(modules)} top-level module(s), {len(tests)} test file(s)." + + return { + 'summary': summary, + 'package_name': pkg_name, + 'packages': packages, + 'top_level_modules': modules, + 'tests': tests[:20], + 'notable_files': notes, + } + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description='Summarize repository structure') + ap.add_argument('--workspace-root', '-w', default='.', help='Repository root') + ap.add_argument('--depth', '-d', type=int, default=2, help='Directory depth for package discovery') + ap.add_argument('--pretty', action='store_true', help='Pretty-print JSON') + args = ap.parse_args(argv) + + out = summarize(args.workspace_root, args.depth) + if args.pretty: + print(json.dumps(out, indent=2, ensure_ascii=False)) + else: + print(json.dumps(out, ensure_ascii=False)) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main())