afterpython 0.1.0.dev7__py3-none-any.whl → 0.1.0.dev9__py3-none-any.whl

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.
Files changed (44) hide show
  1. afterpython/_io/toml.py +5 -3
  2. afterpython/_paths.py +1 -1
  3. afterpython/_typing.py +1 -2
  4. afterpython/builders/__init__.py +0 -1
  5. afterpython/builders/metadata.py +1 -1
  6. afterpython/builders/molab.py +2 -1
  7. afterpython/cli/__init__.py +0 -1
  8. afterpython/cli/commands/add.py +67 -0
  9. afterpython/cli/commands/build.py +10 -9
  10. afterpython/cli/commands/clean.py +2 -1
  11. afterpython/cli/commands/commit.py +16 -1
  12. afterpython/cli/commands/dev.py +3 -2
  13. afterpython/cli/commands/init.py +26 -12
  14. afterpython/cli/commands/init_branch_rules.py +9 -0
  15. afterpython/cli/commands/install.py +8 -2
  16. afterpython/cli/commands/lock.py +23 -0
  17. afterpython/cli/commands/remove.py +67 -0
  18. afterpython/cli/commands/start.py +6 -3
  19. afterpython/cli/commands/sync.py +3 -3
  20. afterpython/cli/commands/update.py +26 -3
  21. afterpython/cli/main.py +75 -14
  22. afterpython/const.py +0 -1
  23. afterpython/pcu.py +3 -1
  24. afterpython/templates/ci-workflow-template.yml +197 -0
  25. afterpython/templates/dependabot-template.yml +30 -0
  26. afterpython/templates/deploy-workflow-template.yml +5 -5
  27. afterpython/templates/release-workflow-template.yml +3 -3
  28. afterpython/templates/ruff-template.toml +1 -1
  29. afterpython/tools/_afterpython.py +2 -1
  30. afterpython/tools/_git.py +28 -2
  31. afterpython/tools/branch_rules.py +106 -0
  32. afterpython/tools/commitizen.py +1 -2
  33. afterpython/tools/github_actions.py +23 -53
  34. afterpython/tools/myst.py +4 -2
  35. afterpython/tools/pre_commit.py +1 -3
  36. afterpython/tools/pyproject.py +4 -2
  37. afterpython/utils.py +9 -0
  38. {afterpython-0.1.0.dev7.dist-info → afterpython-0.1.0.dev9.dist-info}/METADATA +18 -8
  39. afterpython-0.1.0.dev9.dist-info/RECORD +52 -0
  40. {afterpython-0.1.0.dev7.dist-info → afterpython-0.1.0.dev9.dist-info}/WHEEL +1 -1
  41. afterpython/.pre-commit-config.yaml +0 -48
  42. afterpython/cz.toml +0 -27
  43. afterpython-0.1.0.dev7.dist-info/RECORD +0 -47
  44. {afterpython-0.1.0.dev7.dist-info → afterpython-0.1.0.dev9.dist-info}/entry_points.txt +0 -0
afterpython/cli/main.py CHANGED
@@ -1,32 +1,89 @@
1
1
  import os
2
2
  import subprocess
3
+ from collections import defaultdict
3
4
 
4
5
  import click
6
+ from dotenv import find_dotenv, load_dotenv
5
7
  from trogon import tui
6
- from dotenv import load_dotenv, find_dotenv
7
8
 
8
9
  import afterpython as ap
9
10
  from afterpython import __version__
10
- from afterpython.cli.commands.init import init
11
+ from afterpython.cli.commands.add import add
11
12
  from afterpython.cli.commands.build import build
12
- from afterpython.cli.commands.dev import dev
13
- from afterpython.cli.commands.update import update
13
+ from afterpython.cli.commands.bump import bump
14
14
  from afterpython.cli.commands.check import check
15
- from afterpython.cli.commands.install import install
16
- from afterpython.cli.commands.format import format
17
- from afterpython.cli.commands.sync import sync
18
- from afterpython.cli.commands.start import start, doc, blog, tutorial, example, guide
19
- from afterpython.cli.commands.preview import preview
20
15
  from afterpython.cli.commands.clean import clean
21
- from afterpython.cli.commands.pre_commit import pre_commit
22
- from afterpython.cli.commands.commitizen import commitizen
23
16
  from afterpython.cli.commands.commit import commit
24
- from afterpython.cli.commands.bump import bump
17
+ from afterpython.cli.commands.commitizen import commitizen
18
+ from afterpython.cli.commands.dev import dev
19
+ from afterpython.cli.commands.format import format
20
+ from afterpython.cli.commands.init import init
21
+ from afterpython.cli.commands.init_branch_rules import init_branch_rules
22
+ from afterpython.cli.commands.install import install
23
+ from afterpython.cli.commands.lock import lock
24
+ from afterpython.cli.commands.pre_commit import pre_commit
25
+ from afterpython.cli.commands.preview import preview
25
26
  from afterpython.cli.commands.release import release
27
+ from afterpython.cli.commands.remove import remove
28
+ from afterpython.cli.commands.start import blog, doc, example, guide, start, tutorial
29
+ from afterpython.cli.commands.sync import sync
30
+ from afterpython.cli.commands.update import update
31
+
32
+
33
+ class AliasGroup(click.Group):
34
+ """Custom group that displays command aliases together."""
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ super().__init__(*args, **kwargs)
38
+ self._aliases = defaultdict(list) # Maps primary command -> list of aliases
39
+ self._alias_to_primary = {} # Maps alias -> primary command name
40
+
41
+ def add_command(self, cmd, name=None):
42
+ """Track aliases when the same command is added with different names."""
43
+ name = name or cmd.name
44
+
45
+ # Check if this command object was already added under a different name
46
+ for existing_name, existing_cmd in self.commands.items():
47
+ if existing_cmd is cmd and existing_name != name:
48
+ # This is an alias
49
+ self._aliases[existing_name].append(name)
50
+ self._alias_to_primary[name] = existing_name
51
+ break
52
+
53
+ super().add_command(cmd, name)
54
+
55
+ def format_commands(self, ctx, formatter):
56
+ """Format commands with aliases shown together."""
57
+ commands = []
58
+ for subcommand in self.list_commands(ctx):
59
+ # Skip aliases - they'll be shown with their primary command
60
+ if subcommand in self._alias_to_primary:
61
+ continue
62
+
63
+ cmd = self.get_command(ctx, subcommand)
64
+ if cmd is None:
65
+ continue
66
+
67
+ # Skip hidden commands
68
+ if cmd.hidden:
69
+ continue
70
+
71
+ help_text = cmd.get_short_help_str(limit=formatter.width)
72
+
73
+ # Build command name with aliases
74
+ aliases = self._aliases.get(subcommand, [])
75
+ if aliases:
76
+ subcommand = f"{subcommand}, {', '.join(aliases)}"
77
+
78
+ commands.append((subcommand, help_text))
79
+
80
+ if commands:
81
+ with formatter.section("Commands"):
82
+ formatter.write_dl(commands)
26
83
 
27
84
 
28
85
  @tui(command="tui", help="Open terminal UI")
29
- @click.group(context_settings={"help_option_names": ["-h", "--help"]})
86
+ @click.group(cls=AliasGroup, context_settings={"help_option_names": ["-h", "--help"]})
30
87
  @click.pass_context
31
88
  @click.version_option(version=__version__)
32
89
  def afterpython_group(ctx):
@@ -50,6 +107,7 @@ afterpython_group.add_command(build)
50
107
  afterpython_group.add_command(dev)
51
108
  afterpython_group.add_command(update)
52
109
  afterpython_group.add_command(check)
110
+ afterpython_group.add_command(check, name="lint")
53
111
  afterpython_group.add_command(format)
54
112
  afterpython_group.add_command(sync)
55
113
  afterpython_group.add_command(start)
@@ -63,9 +121,12 @@ afterpython_group.add_command(preview)
63
121
  afterpython_group.add_command(clean)
64
122
  afterpython_group.add_command(pre_commit)
65
123
  afterpython_group.add_command(pre_commit, name="pc")
66
- afterpython_group.add_command(pre_commit, name="precommit")
67
124
  afterpython_group.add_command(commitizen)
68
125
  afterpython_group.add_command(commitizen, name="cz")
69
126
  afterpython_group.add_command(commit)
70
127
  afterpython_group.add_command(bump)
71
128
  afterpython_group.add_command(release)
129
+ afterpython_group.add_command(init_branch_rules)
130
+ afterpython_group.add_command(add)
131
+ afterpython_group.add_command(remove)
132
+ afterpython_group.add_command(lock)
afterpython/const.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from afterpython._typing import tContentType
2
2
 
3
-
4
3
  NODEENV_VERSION = "24.11.0"
5
4
  CONTENT_TYPES: set[tContentType] = {"doc", "blog", "tutorial", "example", "guide"}
afterpython/pcu.py CHANGED
@@ -3,7 +3,8 @@ pcu = "pip check updates", similar to ncu (npm check updates in Node.js)
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
- from typing import TYPE_CHECKING, TypedDict, NamedTuple, TypeAlias
6
+
7
+ from typing import TYPE_CHECKING, NamedTuple, TypeAlias, TypedDict
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from tomlkit.toml_document import TOMLDocument
@@ -60,6 +61,7 @@ async def get_latest_versions(
60
61
  ) -> dict[str, Version | None]:
61
62
  """Get latest versions for a list of dependencies from PyPI."""
62
63
  import httpx
64
+
63
65
  from afterpython.utils import fetch_pypi_json
64
66
 
65
67
  async def fetch_version(
@@ -0,0 +1,197 @@
1
+ name: CI
2
+
3
+ # Trigger CI on pushes to main and all pull requests
4
+ # This ensures every code change is validated before merging
5
+ on:
6
+ push:
7
+ branches:
8
+ - main
9
+ pull_request:
10
+ # Run on all PRs regardless of target branch
11
+ branches:
12
+ - '**'
13
+
14
+ jobs:
15
+ # Job 1: Lint and Format Check
16
+ # Runs ruff to check code quality and formatting
17
+ lint:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v6
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v6
25
+ with:
26
+ python-version: '3.11'
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v7
30
+ with:
31
+ version: "latest"
32
+
33
+ - name: Install dependencies
34
+ # Install the package with uv in system mode
35
+ # This installs ruff and other tools defined in dependencies
36
+ run: uv pip install --system .
37
+
38
+ - name: Run ruff check
39
+ run: ruff check --config ./afterpython/ruff.toml .
40
+
41
+ - name: Run ruff format check
42
+ # --check flag ensures it only validates, doesn't reformat
43
+ run: ruff format --check --config ./afterpython/ruff.toml .
44
+
45
+ # Job 2: Detect Test Strategy
46
+ # Determines whether to use pixi or uv based on pixi.toml presence
47
+ detect-test-strategy:
48
+ runs-on: ubuntu-latest
49
+ outputs:
50
+ use_pixi: ${{ steps.check.outputs.use_pixi }}
51
+ steps:
52
+ - name: Checkout code
53
+ uses: actions/checkout@v6
54
+
55
+ - name: Check for pixi.toml
56
+ id: check
57
+ run: |
58
+ if [ -f "pixi.toml" ]; then
59
+ echo "use_pixi=true" >> $GITHUB_OUTPUT
60
+ else
61
+ echo "use_pixi=false" >> $GITHUB_OUTPUT
62
+ fi
63
+
64
+ # Job 3.1: Test Suite - UV Workflow
65
+ # Traditional Python testing with uv for projects without pixi
66
+ test-uv:
67
+ needs: detect-test-strategy
68
+ if: needs.detect-test-strategy.outputs.use_pixi == 'false'
69
+ runs-on: ubuntu-latest
70
+ strategy:
71
+ matrix:
72
+ # TODO: support python 3.14
73
+ python-version: ['3.11', '3.12', '3.13']
74
+ fail-fast: false
75
+
76
+ steps:
77
+ - name: Checkout code
78
+ uses: actions/checkout@v6
79
+
80
+ - name: Set up Python ${{ matrix.python-version }}
81
+ uses: actions/setup-python@v6
82
+ with:
83
+ python-version: ${{ matrix.python-version }}
84
+
85
+ - name: Install uv
86
+ uses: astral-sh/setup-uv@v7
87
+ with:
88
+ version: "latest"
89
+
90
+ - name: Install dependencies
91
+ run: |
92
+ # Install package, main dependencies, and test dependencies
93
+ # If no [dependency-groups] test exists, only the package and main dependencies are installed
94
+ uv pip install --system . --group test || uv pip install --system .
95
+
96
+ - name: Run tests with pytest
97
+ # Run pytest with verbose output
98
+ # Exit code 5 (no tests collected) is treated as success
99
+ run: |
100
+ pytest -v || if [ $? -eq 5 ]; then exit 0; else exit $?; fi
101
+
102
+ # Job 3.2: Test Suite - Pixi Workflow
103
+ # Pixi-based testing for projects using pixi.toml
104
+ test-pixi:
105
+ needs: detect-test-strategy
106
+ if: needs.detect-test-strategy.outputs.use_pixi == 'true'
107
+ runs-on: ubuntu-latest
108
+ strategy:
109
+ matrix:
110
+ # TODO: support python 3.14
111
+ environment: ['py311', 'py312', 'py313']
112
+ fail-fast: false
113
+
114
+ steps:
115
+ - name: Checkout code
116
+ uses: actions/checkout@v6
117
+
118
+ - name: Set up pixi
119
+ uses: prefix-dev/setup-pixi@v0.9.3
120
+ with:
121
+ pixi-version: v0.59.0
122
+ cache: true
123
+ # Optional: Uncomment if you need prefix.dev authentication
124
+ # auth-host: prefix.dev
125
+ # auth-token: ${{ secrets.PREFIX_DEV_TOKEN }}
126
+
127
+ - name: Run tests with pixi
128
+ # Runs test in specific Python environment (py311, py312, py313)
129
+ # Assumes you have environments defined in pixi.toml with a "test" task
130
+ # Example pixi.toml:
131
+ # [environments]
132
+ # py311 = ["py311", "test"]
133
+ # [feature.test.tasks]
134
+ # test = "pytest -v"
135
+ # Note: Exit code 5 (no tests collected) is treated as success
136
+ run: |
137
+ pixi run -e ${{ matrix.environment }} test || if [ $? -eq 5 ]; then exit 0; else exit $?; fi
138
+
139
+ # Job 3.3: Test Suite - Complete
140
+ # Unified test status for branch protection
141
+ test:
142
+ if: always()
143
+ needs: [detect-test-strategy, test-uv, test-pixi]
144
+ runs-on: ubuntu-latest
145
+ steps:
146
+ - name: Check test results
147
+ run: |
148
+ uv_result="${{ needs.test-uv.result }}"
149
+ pixi_result="${{ needs.test-pixi.result }}"
150
+ # One should succeed, the other should be skipped
151
+ if [ "$uv_result" == "success" ] || [ "$uv_result" == "skipped" ]; then
152
+ if [ "$pixi_result" == "success" ] || [ "$pixi_result" == "skipped" ]; then
153
+ echo "All tests passed!"
154
+ exit 0
155
+ fi
156
+ fi
157
+ echo "Tests failed: uv=$uv_result, pixi=$pixi_result"
158
+ exit 1
159
+
160
+ # Job 4: Build Verification
161
+ # Ensures the package can be built successfully
162
+ build:
163
+ runs-on: ubuntu-latest
164
+ steps:
165
+ - name: Checkout code
166
+ uses: actions/checkout@v6
167
+
168
+ - name: Set up Python
169
+ uses: actions/setup-python@v6
170
+ with:
171
+ python-version: '3.11'
172
+
173
+ - name: Install uv
174
+ uses: astral-sh/setup-uv@v7
175
+ with:
176
+ version: "latest"
177
+
178
+ - name: Build package
179
+ run: uv build
180
+
181
+ - name: List build artifacts
182
+ # Show what was built (wheel + sdist)
183
+ # Useful for debugging build issues
184
+ run: ls -lh dist/
185
+
186
+ - name: Install built package
187
+ # Test that the built wheel can be installed
188
+ # This catches packaging issues like missing files
189
+ run: uv pip install --system dist/*.whl
190
+
191
+ - name: Verify installation
192
+ # Confirm the CLI entry point works
193
+ # This ensures the package is usable after installation
194
+ run: ap --help
195
+
196
+ # Job 5: Type Check
197
+ # typecheck:
@@ -0,0 +1,30 @@
1
+ # Dependabot configuration for automatic dependency updates
2
+ # See: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3
+
4
+ version: 2
5
+ updates:
6
+ # Update GitHub Actions versions
7
+ # This keeps your workflow actions secure and up-to-date
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/"
10
+ schedule:
11
+ # Check for updates monthly (less noisy than weekly)
12
+ interval: "monthly"
13
+ labels:
14
+ - "dependencies"
15
+ - "github-actions"
16
+ # Group all GitHub Actions updates into a single PR
17
+ # This reduces PR noise and makes reviewing easier
18
+ groups:
19
+ github-actions:
20
+ patterns:
21
+ - "*"
22
+ # Automatically add reviewers (optional - uncomment and customize)
23
+ # reviewers:
24
+ # - "your-username"
25
+ # Limit how many Dependabot PRs can be open at once (optional)
26
+ # Default is 5, increase if you want more concurrent PRs
27
+ # open-pull-requests-limit: 10
28
+ #
29
+ # Note: Auto-merge is configured in GitHub repo settings, not here.
30
+ # Go to: Settings → General → Pull Requests → Allow auto-merge
@@ -35,7 +35,7 @@ jobs:
35
35
  runs-on: ubuntu-latest
36
36
  steps:
37
37
  - name: Checkout code
38
- uses: actions/checkout@v4
38
+ uses: actions/checkout@v6
39
39
 
40
40
  - name: Install pnpm
41
41
  uses: pnpm/action-setup@v4
@@ -43,19 +43,19 @@ jobs:
43
43
  version: 10
44
44
 
45
45
  - name: Set up Node.js
46
- uses: actions/setup-node@v4
46
+ uses: actions/setup-node@v6
47
47
  with:
48
48
  node-version: '24'
49
49
  cache: 'pnpm'
50
50
  cache-dependency-path: './afterpython/_website/pnpm-lock.yaml'
51
51
 
52
52
  - name: Set up Python
53
- uses: actions/setup-python@v5
53
+ uses: actions/setup-python@v6
54
54
  with:
55
55
  python-version: '3.11'
56
56
 
57
57
  - name: Install uv
58
- uses: astral-sh/setup-uv@v5
58
+ uses: astral-sh/setup-uv@v7
59
59
  with:
60
60
  version: "latest"
61
61
 
@@ -71,7 +71,7 @@ jobs:
71
71
  ap build
72
72
 
73
73
  - name: Upload artifact
74
- uses: actions/upload-pages-artifact@v3
74
+ uses: actions/upload-pages-artifact@v4
75
75
  with:
76
76
  path: './afterpython/_website/build'
77
77
 
@@ -14,15 +14,15 @@ jobs:
14
14
 
15
15
  steps:
16
16
  - name: Checkout code
17
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v6
18
18
 
19
19
  - name: Set up Python
20
- uses: actions/setup-python@v5
20
+ uses: actions/setup-python@v6
21
21
  with:
22
22
  python-version: '3.11'
23
23
 
24
24
  - name: Install uv
25
- uses: astral-sh/setup-uv@v5
25
+ uses: astral-sh/setup-uv@v7
26
26
  with:
27
27
  version: "latest"
28
28
 
@@ -18,7 +18,7 @@ ignore = [
18
18
  ]
19
19
  # extended rules:
20
20
  extend-select = [
21
- # "I", # isort - import sorting
21
+ "I", # isort - import sorting
22
22
  "UP", # pyupgrade - upgrade syntax for newer Python
23
23
  "B", # flake8-bugbear - find likely bugs
24
24
  "SIM", # flake8-simplify - simplify code
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -7,7 +8,7 @@ if TYPE_CHECKING:
7
8
  import tomlkit
8
9
 
9
10
  import afterpython as ap
10
- from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
11
+ from afterpython._io.toml import _to_tomlkit, read_toml, write_toml
11
12
 
12
13
 
13
14
  def read_afterpython() -> TOMLDocument:
afterpython/tools/_git.py CHANGED
@@ -58,7 +58,7 @@ def get_github_url() -> str | None:
58
58
  return None
59
59
 
60
60
 
61
- def setup_github_auth():
61
+ def is_gh_authenticated():
62
62
  """Guide user through GitHub authentication."""
63
63
  if not has_gh():
64
64
  print("""
@@ -89,7 +89,6 @@ Please authenticate with GitHub:
89
89
  """)
90
90
  return False
91
91
 
92
- print("✓ GitHub authentication verified")
93
92
  return True
94
93
 
95
94
 
@@ -107,5 +106,32 @@ def get_github_username() -> str | None:
107
106
  return result.stdout.strip()
108
107
 
109
108
 
109
+ def get_current_user_id() -> int | None:
110
+ """
111
+ Get the GitHub user ID of the currently authenticated user.
112
+
113
+ Returns:
114
+ User ID or None if error
115
+ """
116
+ if not is_gh_authenticated():
117
+ return None
118
+
119
+ result = subprocess.run(
120
+ ["gh", "api", "user", "--jq", ".id"],
121
+ capture_output=True,
122
+ text=True,
123
+ )
124
+
125
+ if result.returncode != 0:
126
+ print(f"Error fetching user ID: {result.stderr}")
127
+ return None
128
+
129
+ try:
130
+ return int(result.stdout.strip())
131
+ except ValueError:
132
+ print("Error: Failed to parse user ID")
133
+ return None
134
+
135
+
110
136
  # TODO: use gh's token for pygithub to get repo issues
111
137
  # def get_repo_issues(owner: str, repo: str) -> list[dict]:
@@ -0,0 +1,106 @@
1
+ """
2
+ Branch protection rules for AfterPython projects.
3
+ Uses gh CLI to create GitHub rulesets.
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+ from copy import deepcopy
9
+
10
+ from afterpython.tools._git import is_gh_authenticated
11
+
12
+ DEFAULT_RULESET = {
13
+ "name": "afterpython-default", # The ruleset identifier in GitHub
14
+ "target": "branch", # This ruleset applies to branches (not tags)
15
+ "enforcement": "active", # Rules are enforced (vs "disabled" or "evaluate")
16
+ "conditions": { # Which branches this ruleset applies to
17
+ "ref_name": {
18
+ "include": ["refs/heads/main"], # Only protect the 'main' branch
19
+ "exclude": [], # No exclusions
20
+ }
21
+ },
22
+ "bypass_actors": [
23
+ {
24
+ "actor_id": 5, # Repository admin role
25
+ "actor_type": "RepositoryRole",
26
+ "bypass_mode": "always",
27
+ }
28
+ ],
29
+ "rules": [ # Array of protection rules
30
+ # Rule 1: No Force Pushes
31
+ {"type": "non_fast_forward"}, # Prevents git push --force
32
+ # Rule 2: No Branch Deletion
33
+ {"type": "deletion"}, # Prevents branch from being deleted
34
+ # Rule 3: CI Status Checks
35
+ {
36
+ "type": "required_status_checks",
37
+ "parameters": {
38
+ "required_status_checks": [
39
+ {"context": "lint"},
40
+ {"context": "test"},
41
+ {"context": "build"},
42
+ ],
43
+ # The PR branch must be up to date with the base branch (main) before merging
44
+ "strict_required_status_checks_policy": True,
45
+ },
46
+ },
47
+ ],
48
+ }
49
+
50
+
51
+ def list_rulesets() -> list[dict] | None:
52
+ """
53
+ List existing rulesets for the current repository.
54
+
55
+ Returns:
56
+ List of rulesets or None if error
57
+ """
58
+ if not is_gh_authenticated():
59
+ return None
60
+
61
+ result = subprocess.run(
62
+ ["gh", "api", "repos/{owner}/{repo}/rulesets"],
63
+ capture_output=True,
64
+ text=True,
65
+ )
66
+
67
+ if result.returncode != 0:
68
+ print(f"Error listing rulesets: {result.stderr}")
69
+ return None
70
+
71
+ try:
72
+ return json.loads(result.stdout)
73
+ except json.JSONDecodeError:
74
+ print("Error: Failed to parse rulesets JSON")
75
+ return None
76
+
77
+
78
+ def create_default_branch_rules(branch: str = "main") -> bool:
79
+ if not is_gh_authenticated():
80
+ return False
81
+
82
+ # Check if ruleset already exists
83
+ existing_rulesets = list_rulesets()
84
+ if existing_rulesets:
85
+ print(f"Ruleset '{DEFAULT_RULESET['name']}' already exists.")
86
+ return False
87
+
88
+ # Build payload
89
+ payload = deepcopy(DEFAULT_RULESET)
90
+ if branch != "main":
91
+ payload["conditions"]["ref_name"]["include"] = [f"refs/heads/{branch}"]
92
+
93
+ # Create ruleset
94
+ result = subprocess.run(
95
+ ["gh", "api", "-X", "POST", "repos/{owner}/{repo}/rulesets", "--input", "-"],
96
+ input=json.dumps(payload),
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+
101
+ if result.returncode != 0:
102
+ print(f"Error creating ruleset: {result.stderr}")
103
+ return False
104
+
105
+ print(f"✓ Created branch ruleset '{DEFAULT_RULESET['name']}' for {branch=}")
106
+ return True
@@ -1,6 +1,5 @@
1
1
  import afterpython as ap
2
2
 
3
-
4
3
  commitizen_default = {
5
4
  "tool": {
6
5
  "commitizen": {
@@ -36,8 +35,8 @@ commitizen_pre_commit_hook = {
36
35
 
37
36
 
38
37
  def init_commitizen():
39
- from afterpython.tools.pre_commit import update_pre_commit
40
38
  from afterpython._io.toml import write_toml
39
+ from afterpython.tools.pre_commit import update_pre_commit
41
40
 
42
41
  afterpython_path = ap.paths.afterpython_path
43
42
  commitizen_toml_path = afterpython_path / "cz.toml"