apdev 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.
apdev-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: apdev
3
+ Version: 0.1.0
4
+ Summary: Shared development tools for Python projects - character validation, circular import detection, and more
5
+ Author-email: aipartnerup <tercel.yi@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://aipartnerup.com
8
+ Project-URL: Documentation, https://github.com/aipartnerup/apdev
9
+ Project-URL: Repository, https://github.com/aipartnerup/apdev
10
+ Project-URL: Issues, https://github.com/aipartnerup/apdev/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
28
+
29
+ # apdev (Python)
30
+
31
+ Shared development tools for Python projects.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install apdev
37
+ ```
38
+
39
+ For development:
40
+
41
+ ```bash
42
+ pip install apdev[dev]
43
+ ```
44
+
45
+ ## Tools
46
+
47
+ ### check-chars
48
+
49
+ Validate that files contain only allowed characters (ASCII, common emoji, and technical symbols like arrows and box-drawing characters).
50
+
51
+ ```bash
52
+ # Check specific files
53
+ apdev check-chars src/mypackage/*.py
54
+
55
+ # Use with pre-commit (see below)
56
+ ```
57
+
58
+ ### check-imports
59
+
60
+ Detect circular imports in a Python package.
61
+
62
+ ```bash
63
+ # Specify package explicitly
64
+ apdev check-imports --package mypackage --src-dir src
65
+
66
+ # Or configure in pyproject.toml (see below)
67
+ apdev check-imports
68
+ ```
69
+
70
+ ### release
71
+
72
+ Interactive release automation for publishing Python packages to PyPI and GitHub. Auto-detects project name from `pyproject.toml` and GitHub repo from git remote.
73
+
74
+ ```bash
75
+ # Run with auto-detected version from pyproject.toml
76
+ apdev release
77
+
78
+ # Or specify version explicitly
79
+ apdev release 0.2.0
80
+ ```
81
+
82
+ The command provides an interactive menu with steps:
83
+ 1. Version verification (checks `pyproject.toml` and `__init__.py` match)
84
+ 2. Status check (tag, build files, PyPI)
85
+ 3. Clean build files
86
+ 4. Build package (`python -m build`)
87
+ 5. Check package (`twine check`)
88
+ 6. Create git tag and push
89
+ 7. Create GitHub release (via `gh` CLI or API)
90
+ 8. Upload to PyPI (`twine upload`)
91
+
92
+ Override auto-detection with environment variables:
93
+
94
+ ```bash
95
+ PROJECT_NAME=mypackage GITHUB_REPO=owner/repo apdev release
96
+ ```
97
+
98
+ ## Configuration
99
+
100
+ Add to your project's `pyproject.toml`:
101
+
102
+ ```toml
103
+ [tool.apdev]
104
+ base_package = "mypackage"
105
+ src_dir = "src"
106
+ ```
107
+
108
+ ## Pre-commit Integration
109
+
110
+ ```yaml
111
+ repos:
112
+ - repo: https://github.com/aipartnerup/apdev
113
+ rev: python/v0.1.0
114
+ hooks:
115
+ - id: check-chars
116
+ - id: check-imports
117
+ ```
118
+
119
+ Or use as a local hook with the pip-installed package:
120
+
121
+ ```yaml
122
+ repos:
123
+ - repo: local
124
+ hooks:
125
+ - id: check-chars
126
+ name: apdev check-chars
127
+ entry: apdev check-chars
128
+ language: system
129
+ types_or: [text, python]
130
+
131
+ - id: check-imports
132
+ name: apdev check-imports
133
+ entry: apdev check-imports
134
+ language: system
135
+ pass_filenames: false
136
+ always_run: true
137
+ ```
138
+
139
+ ## License
140
+
141
+ Apache-2.0
apdev-0.1.0/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # apdev (Python)
2
+
3
+ Shared development tools for Python projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install apdev
9
+ ```
10
+
11
+ For development:
12
+
13
+ ```bash
14
+ pip install apdev[dev]
15
+ ```
16
+
17
+ ## Tools
18
+
19
+ ### check-chars
20
+
21
+ Validate that files contain only allowed characters (ASCII, common emoji, and technical symbols like arrows and box-drawing characters).
22
+
23
+ ```bash
24
+ # Check specific files
25
+ apdev check-chars src/mypackage/*.py
26
+
27
+ # Use with pre-commit (see below)
28
+ ```
29
+
30
+ ### check-imports
31
+
32
+ Detect circular imports in a Python package.
33
+
34
+ ```bash
35
+ # Specify package explicitly
36
+ apdev check-imports --package mypackage --src-dir src
37
+
38
+ # Or configure in pyproject.toml (see below)
39
+ apdev check-imports
40
+ ```
41
+
42
+ ### release
43
+
44
+ Interactive release automation for publishing Python packages to PyPI and GitHub. Auto-detects project name from `pyproject.toml` and GitHub repo from git remote.
45
+
46
+ ```bash
47
+ # Run with auto-detected version from pyproject.toml
48
+ apdev release
49
+
50
+ # Or specify version explicitly
51
+ apdev release 0.2.0
52
+ ```
53
+
54
+ The command provides an interactive menu with steps:
55
+ 1. Version verification (checks `pyproject.toml` and `__init__.py` match)
56
+ 2. Status check (tag, build files, PyPI)
57
+ 3. Clean build files
58
+ 4. Build package (`python -m build`)
59
+ 5. Check package (`twine check`)
60
+ 6. Create git tag and push
61
+ 7. Create GitHub release (via `gh` CLI or API)
62
+ 8. Upload to PyPI (`twine upload`)
63
+
64
+ Override auto-detection with environment variables:
65
+
66
+ ```bash
67
+ PROJECT_NAME=mypackage GITHUB_REPO=owner/repo apdev release
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ Add to your project's `pyproject.toml`:
73
+
74
+ ```toml
75
+ [tool.apdev]
76
+ base_package = "mypackage"
77
+ src_dir = "src"
78
+ ```
79
+
80
+ ## Pre-commit Integration
81
+
82
+ ```yaml
83
+ repos:
84
+ - repo: https://github.com/aipartnerup/apdev
85
+ rev: python/v0.1.0
86
+ hooks:
87
+ - id: check-chars
88
+ - id: check-imports
89
+ ```
90
+
91
+ Or use as a local hook with the pip-installed package:
92
+
93
+ ```yaml
94
+ repos:
95
+ - repo: local
96
+ hooks:
97
+ - id: check-chars
98
+ name: apdev check-chars
99
+ entry: apdev check-chars
100
+ language: system
101
+ types_or: [text, python]
102
+
103
+ - id: check-imports
104
+ name: apdev check-imports
105
+ entry: apdev check-imports
106
+ language: system
107
+ pass_filenames: false
108
+ always_run: true
109
+ ```
110
+
111
+ ## License
112
+
113
+ Apache-2.0
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "apdev"
7
+ version = "0.1.0"
8
+ description = "Shared development tools for Python projects - character validation, circular import detection, and more"
9
+ readme = "README.md"
10
+ license = {text = "Apache-2.0"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "aipartnerup", email = "tercel.yi@gmail.com"}
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Topic :: Software Development :: Quality Assurance",
20
+ "Topic :: Software Development :: Testing",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ ]
27
+ dependencies = [
28
+ "tomli>=1.1.0;python_version<'3.11'",
29
+ ]
30
+
31
+ [project.scripts]
32
+ apdev = "apdev.cli:main"
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=7.0",
37
+ "pytest-cov>=4.0",
38
+ "ruff>=0.1.0",
39
+ ]
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ apdev = ["release.sh"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ruff]
51
+ target-version = "py310"
52
+ line-length = 100
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "W"]
56
+
57
+ [project.urls]
58
+ Homepage = "https://aipartnerup.com"
59
+ Documentation = "https://github.com/aipartnerup/apdev"
60
+ Repository = "https://github.com/aipartnerup/apdev"
61
+ Issues = "https://github.com/aipartnerup/apdev/issues"
apdev-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """apdev - Shared development tools for Python projects."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """Allow running apdev as `python -m apdev`."""
2
+
3
+ import sys
4
+
5
+ from apdev.cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,79 @@
1
+ """Character validation tool.
2
+
3
+ Checks that files contain only allowed characters: ASCII, common emoji,
4
+ and standard technical symbols (arrows, box-drawing, math operators, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ # Allowed Unicode ranges beyond ASCII (0-127)
12
+ EMOJI_RANGES: list[tuple[int, int]] = [
13
+ (0x1F300, 0x1F5FF), # Symbols and Pictographs
14
+ (0x1F600, 0x1F64F), # Emoticons
15
+ (0x1F680, 0x1F6FF), # Transport and Map Symbols
16
+ (0x1F780, 0x1F7FF), # Geometric Shapes Extended
17
+ (0x1F900, 0x1F9FF), # Supplemental Symbols and Pictographs
18
+ (0x2600, 0x26FF), # Miscellaneous Symbols
19
+ (0x2700, 0x27BF), # Dingbats
20
+ ]
21
+
22
+ EXTRA_ALLOWED_RANGES: list[tuple[int, int]] = [
23
+ (0x0080, 0x00FF), # Latin-1 Supplement
24
+ (0x2000, 0x206F), # General Punctuation
25
+ (0x2100, 0x214F), # Letterlike Symbols
26
+ (0x2190, 0x21FF), # Arrows
27
+ (0x2200, 0x22FF), # Mathematical Operators
28
+ (0x2300, 0x23FF), # Miscellaneous Technical
29
+ (0x2500, 0x257F), # Box Drawing
30
+ (0x25A0, 0x25FF), # Geometric Shapes
31
+ (0x2B00, 0x2BFF), # Miscellaneous Symbols and Arrows
32
+ (0xFE00, 0xFE0F), # Variation Selectors
33
+ ]
34
+
35
+ _ALL_RANGES = EMOJI_RANGES + EXTRA_ALLOWED_RANGES
36
+
37
+
38
+ def is_allowed_char(c: str) -> bool:
39
+ """Return True if the character is in the allowed set."""
40
+ code = ord(c)
41
+ if code <= 127:
42
+ return True
43
+ for start, end in _ALL_RANGES:
44
+ if start <= code <= end:
45
+ return True
46
+ return False
47
+
48
+
49
+ def check_file(path: Path, *, max_problems: int = 5) -> list[str]:
50
+ """Check a single file for illegal characters.
51
+
52
+ Returns a list of problem descriptions (empty if the file is clean).
53
+ """
54
+ problems: list[str] = []
55
+ try:
56
+ content = path.read_text(encoding="utf-8")
57
+ for i, char in enumerate(content, 1):
58
+ if not is_allowed_char(char):
59
+ problems.append(
60
+ f"Illegal character at position {i}: {char!r} (U+{ord(char):04X})"
61
+ )
62
+ if len(problems) >= max_problems:
63
+ break
64
+ except Exception as e:
65
+ problems.append(f"Failed to read file: {e}")
66
+ return problems
67
+
68
+
69
+ def check_paths(paths: list[Path]) -> int:
70
+ """Check multiple files. Returns 0 if all clean, 1 if any have problems."""
71
+ has_error = False
72
+ for path in paths:
73
+ problems = check_file(path)
74
+ if problems:
75
+ has_error = True
76
+ print(f"\n{path} contains illegal characters:")
77
+ for p in problems:
78
+ print(f" {p}")
79
+ return 1 if has_error else 0
@@ -0,0 +1,143 @@
1
+ """Circular import detection tool.
2
+
3
+ Builds a module-level dependency graph within a Python package
4
+ and reports any circular import chains.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import sys
11
+ from collections import defaultdict
12
+ from pathlib import Path
13
+
14
+
15
+ class ImportAnalyzer(ast.NodeVisitor):
16
+ """Collect fully-qualified imports from a single Python file."""
17
+
18
+ def __init__(self) -> None:
19
+ self.imports: set[str] = set()
20
+
21
+ def visit_Import(self, node: ast.Import) -> None:
22
+ for alias in node.names:
23
+ self.imports.add(alias.name)
24
+
25
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
26
+ if node.module:
27
+ self.imports.add(node.module)
28
+
29
+
30
+ def file_to_module(file_path: Path, src_dir: Path) -> str:
31
+ """Convert a file path to a dotted module name."""
32
+ rel = file_path.relative_to(src_dir)
33
+ parts = list(rel.parts)
34
+ if parts[-1] == "__init__.py":
35
+ parts = parts[:-1]
36
+ else:
37
+ parts[-1] = parts[-1].removesuffix(".py")
38
+ return ".".join(parts)
39
+
40
+
41
+ def _resolve_imports(raw_imports: set[str], base_package: str) -> set[str]:
42
+ """Filter imports to those within the base package."""
43
+ resolved: set[str] = set()
44
+ for imp in raw_imports:
45
+ if imp == base_package or imp.startswith(base_package + "."):
46
+ resolved.add(imp)
47
+ return resolved
48
+
49
+
50
+ def build_dependency_graph(
51
+ src_dir: Path,
52
+ *,
53
+ base_package: str,
54
+ ) -> dict[str, set[str]]:
55
+ """Build a module-to-module dependency graph for the given package."""
56
+ graph: dict[str, set[str]] = defaultdict(set)
57
+
58
+ for py_file in src_dir.rglob("*.py"):
59
+ if "__pycache__" in py_file.parts:
60
+ continue
61
+
62
+ module_name = file_to_module(py_file, src_dir)
63
+ if not module_name:
64
+ continue
65
+
66
+ try:
67
+ source = py_file.read_text(encoding="utf-8")
68
+ tree = ast.parse(source, filename=str(py_file))
69
+ except Exception as e:
70
+ print(f"Warning: could not parse {py_file}: {e}", file=sys.stderr)
71
+ continue
72
+
73
+ analyzer = ImportAnalyzer()
74
+ analyzer.visit(tree)
75
+
76
+ deps = _resolve_imports(analyzer.imports, base_package)
77
+ deps.discard(module_name)
78
+ graph[module_name] = deps
79
+
80
+ return graph
81
+
82
+
83
+ def find_cycles(graph: dict[str, set[str]]) -> list[list[str]]:
84
+ """Find all elementary cycles in the dependency graph using DFS."""
85
+ WHITE, GRAY, BLACK = 0, 1, 2
86
+ color: dict[str, int] = defaultdict(int)
87
+ path: list[str] = []
88
+ cycles: list[list[str]] = []
89
+
90
+ def dfs(node: str) -> None:
91
+ color[node] = GRAY
92
+ path.append(node)
93
+ for neighbor in sorted(graph.get(node, set())):
94
+ if color[neighbor] == GRAY and neighbor in path:
95
+ idx = path.index(neighbor)
96
+ cycle = path[idx:] + [neighbor]
97
+ cycles.append(cycle)
98
+ elif color[neighbor] == WHITE:
99
+ dfs(neighbor)
100
+ path.pop()
101
+ color[node] = BLACK
102
+
103
+ for node in sorted(graph):
104
+ if color[node] == WHITE:
105
+ dfs(node)
106
+
107
+ # Deduplicate by normalizing cycle rotation
108
+ unique: list[list[str]] = []
109
+ seen: set[tuple[str, ...]] = set()
110
+ for cycle in cycles:
111
+ ring = cycle[:-1]
112
+ min_idx = ring.index(min(ring))
113
+ normalized = tuple(ring[min_idx:] + ring[:min_idx])
114
+ if normalized not in seen:
115
+ seen.add(normalized)
116
+ unique.append(cycle)
117
+
118
+ return unique
119
+
120
+
121
+ def check_circular_imports(
122
+ src_dir: Path,
123
+ *,
124
+ base_package: str,
125
+ ) -> int:
126
+ """Run circular import detection. Returns 0 if clean, 1 if cycles found."""
127
+ if not src_dir.exists():
128
+ print(f"Error: {src_dir}/ directory not found", file=sys.stderr)
129
+ return 1
130
+
131
+ graph = build_dependency_graph(src_dir, base_package=base_package)
132
+ print(f"Scanned {len(graph)} modules")
133
+
134
+ cycles = find_cycles(graph)
135
+ if cycles:
136
+ print(f"\nFound {len(cycles)} circular import(s):\n")
137
+ for i, cycle in enumerate(cycles, 1):
138
+ print(f" Cycle {i}: {' -> '.join(cycle)}")
139
+ print()
140
+ return 1
141
+
142
+ print("No circular imports detected.")
143
+ return 0
@@ -0,0 +1,116 @@
1
+ """Command-line interface for apdev."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.resources
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import apdev
13
+ from apdev.check_chars import check_paths
14
+ from apdev.check_imports import check_circular_imports
15
+ from apdev.config import load_config
16
+
17
+
18
+ def _get_release_script() -> Path:
19
+ """Locate the bundled release.sh script."""
20
+ ref = importlib.resources.files("apdev").joinpath("release.sh")
21
+ return Path(str(ref))
22
+
23
+
24
+ def _build_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(
26
+ prog="apdev",
27
+ description="Shared development tools for Python projects",
28
+ )
29
+ parser.add_argument(
30
+ "--version",
31
+ action="version",
32
+ version=f"apdev {apdev.__version__}",
33
+ )
34
+ subparsers = parser.add_subparsers(dest="command")
35
+
36
+ # check-chars
37
+ chars_parser = subparsers.add_parser(
38
+ "check-chars",
39
+ help="Validate files contain only allowed characters",
40
+ )
41
+ chars_parser.add_argument(
42
+ "files",
43
+ nargs="*",
44
+ type=Path,
45
+ help="Files to check",
46
+ )
47
+
48
+ # check-imports
49
+ imports_parser = subparsers.add_parser(
50
+ "check-imports",
51
+ help="Detect circular imports in a Python package",
52
+ )
53
+ imports_parser.add_argument(
54
+ "--package",
55
+ dest="base_package",
56
+ help="Base package name (e.g. apflow). Reads from [tool.apdev] if omitted.",
57
+ )
58
+ imports_parser.add_argument(
59
+ "--src-dir",
60
+ dest="src_dir",
61
+ help="Source directory containing the package (default: src)",
62
+ )
63
+
64
+ # release
65
+ release_parser = subparsers.add_parser(
66
+ "release",
67
+ help="Interactive release automation (build, tag, GitHub release, PyPI upload)",
68
+ )
69
+ release_parser.add_argument(
70
+ "version",
71
+ nargs="?",
72
+ help="Version to release (auto-detected from pyproject.toml if omitted)",
73
+ )
74
+
75
+ return parser
76
+
77
+
78
+ def main(argv: list[str] | None = None) -> int:
79
+ """Entry point for the apdev CLI."""
80
+ parser = _build_parser()
81
+ args = parser.parse_args(argv)
82
+
83
+ if args.command is None:
84
+ parser.print_help()
85
+ return 0
86
+
87
+ if args.command == "check-chars":
88
+ return check_paths(args.files)
89
+
90
+ if args.command == "check-imports":
91
+ config = load_config()
92
+ base_package = args.base_package or config.get("base_package")
93
+ src_dir_str = args.src_dir or config.get("src_dir", "src")
94
+ src_dir = Path(src_dir_str)
95
+
96
+ if not base_package:
97
+ print(
98
+ "Error: --package is required (or set base_package in [tool.apdev])",
99
+ file=sys.stderr,
100
+ )
101
+ return 1
102
+
103
+ return check_circular_imports(src_dir, base_package=base_package)
104
+
105
+ if args.command == "release":
106
+ script = _get_release_script()
107
+ if not script.is_file():
108
+ print("Error: release.sh not found in package", file=sys.stderr)
109
+ return 1
110
+ cmd = ["bash", str(script)]
111
+ if args.version:
112
+ cmd.append(args.version)
113
+ result = subprocess.run(cmd, env={**os.environ})
114
+ return result.returncode
115
+
116
+ return 0