itw-python-builder 0.1.23__tar.gz → 0.1.25__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.
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/PKG-INFO +1 -1
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder/tasks.py +160 -39
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder/version.py +4 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/PKG-INFO +1 -1
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/pyproject.toml +1 -1
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/LICENSE +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/README.md +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder/.pylintrc +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder/__init__.py +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder/cli.py +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/SOURCES.txt +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/dependency_links.txt +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/entry_points.txt +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/requires.txt +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/top_level.txt +0 -0
- {itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/setup.cfg +0 -0
|
@@ -3,12 +3,49 @@ from itw_python_builder.version import Version
|
|
|
3
3
|
import getpass
|
|
4
4
|
import io
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
PYLINTRC = Path(__file__).parent / ".pylintrc"
|
|
10
11
|
_venv_activated = False
|
|
11
12
|
|
|
13
|
+
def detect_project_type() -> str:
|
|
14
|
+
"""Detect whether the current directory is a 'frontend' or 'backend' project."""
|
|
15
|
+
cwd = os.getcwd()
|
|
16
|
+
has_package_json = os.path.isfile(os.path.join(cwd, 'package.json'))
|
|
17
|
+
has_manage_py = os.path.isfile(os.path.join(cwd, 'manage.py'))
|
|
18
|
+
|
|
19
|
+
if has_package_json and has_manage_py:
|
|
20
|
+
raise RuntimeError(
|
|
21
|
+
'Ambiguous project: both package.json and manage.py found in current directory.'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if has_package_json:
|
|
25
|
+
if not os.path.isdir(os.path.join(cwd, 'node_modules')):
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
'Found package.json but no node_modules/ directory.\n'
|
|
28
|
+
'Install dependencies with: npm install'
|
|
29
|
+
)
|
|
30
|
+
return 'frontend'
|
|
31
|
+
|
|
32
|
+
if has_manage_py:
|
|
33
|
+
has_venv = (
|
|
34
|
+
os.path.isdir(os.path.join(cwd, '.venv'))
|
|
35
|
+
or os.path.isdir(os.path.join(cwd, 'venv'))
|
|
36
|
+
)
|
|
37
|
+
if not has_venv:
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
'Found manage.py but no virtual environment (venv or .venv) in current directory.\n'
|
|
40
|
+
'Create one with: python -m venv .venv'
|
|
41
|
+
)
|
|
42
|
+
return 'backend'
|
|
43
|
+
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
'Could not detect project type: no package.json or manage.py in current directory.'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
12
49
|
def detect_and_activate_venv():
|
|
13
50
|
"""Detect venv in current directory and prepend to PATH. Runs only once."""
|
|
14
51
|
global _venv_activated
|
|
@@ -42,8 +79,29 @@ def detect_and_activate_venv():
|
|
|
42
79
|
print(f"[itw] Using venv: {venv_path}")
|
|
43
80
|
_venv_activated = True
|
|
44
81
|
|
|
45
|
-
def
|
|
46
|
-
"""
|
|
82
|
+
def _parse_angular_env_file(path: str) -> dict:
|
|
83
|
+
"""Extract string-valued keys from an Angular environment.ts object literal."""
|
|
84
|
+
with open(path, 'r', encoding='utf-8') as fp:
|
|
85
|
+
content = fp.read()
|
|
86
|
+
pattern = re.compile(r"""^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s*['"]([^'"]*)['"]""", re.MULTILINE)
|
|
87
|
+
return dict(pattern.findall(content))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_env(ctx: Context = None):
|
|
91
|
+
"""Load env vars. Backend → .env in cwd. Frontend → src/environments/environment[.<branch>].ts."""
|
|
92
|
+
if detect_project_type() == 'frontend':
|
|
93
|
+
env_dir = os.path.join(os.getcwd(), 'src', 'environments')
|
|
94
|
+
candidates = []
|
|
95
|
+
if ctx is not None:
|
|
96
|
+
branch = get_current_branch(ctx)
|
|
97
|
+
candidates.append(os.path.join(env_dir, f'environment.{branch}.ts'))
|
|
98
|
+
candidates.append(os.path.join(env_dir, 'environment.ts'))
|
|
99
|
+
env_path = next((p for p in candidates if os.path.exists(p)), None)
|
|
100
|
+
if not env_path:
|
|
101
|
+
return
|
|
102
|
+
for key, value in _parse_angular_env_file(env_path).items():
|
|
103
|
+
os.environ.setdefault(key, value)
|
|
104
|
+
return
|
|
47
105
|
from decouple import Config, RepositoryEnv
|
|
48
106
|
env_path = os.path.join(os.getcwd(), '.env')
|
|
49
107
|
if not os.path.exists(env_path):
|
|
@@ -86,6 +144,39 @@ def get_latest_tag(ctx: Context) -> Version:
|
|
|
86
144
|
return Version.parse(latest_tag)
|
|
87
145
|
|
|
88
146
|
|
|
147
|
+
def _update_package_json_version(version: Version) -> None:
|
|
148
|
+
"""Rewrite only the `"version": "..."` line in package.json, preserving formatting."""
|
|
149
|
+
pkg_path = os.path.join(os.getcwd(), 'package.json')
|
|
150
|
+
with open(pkg_path, 'r', encoding='utf-8') as fp:
|
|
151
|
+
content = fp.read()
|
|
152
|
+
new_content, n = re.subn(
|
|
153
|
+
r'(^\s*"version"\s*:\s*")[^"]*(")',
|
|
154
|
+
lambda m: f'{m.group(1)}{version.bare_semver()}{m.group(2)}',
|
|
155
|
+
content,
|
|
156
|
+
count=1,
|
|
157
|
+
flags=re.MULTILINE,
|
|
158
|
+
)
|
|
159
|
+
if n == 0:
|
|
160
|
+
raise RuntimeError(f'Could not find "version" field in {pkg_path}')
|
|
161
|
+
with open(pkg_path, 'w', encoding='utf-8') as fp:
|
|
162
|
+
fp.write(new_content)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def read_version(ctx: Context) -> Version:
|
|
166
|
+
"""Read current version. Backend → VERSION file. Frontend → latest git tag."""
|
|
167
|
+
if detect_project_type() == 'frontend':
|
|
168
|
+
return get_latest_tag(ctx)
|
|
169
|
+
return Version.read_from_file()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def save_version(version: Version) -> None:
|
|
173
|
+
"""Persist version. Backend → VERSION file. Frontend → package.json (bare semver)."""
|
|
174
|
+
if detect_project_type() == 'frontend':
|
|
175
|
+
_update_package_json_version(version)
|
|
176
|
+
return
|
|
177
|
+
version.save_to_file()
|
|
178
|
+
|
|
179
|
+
|
|
89
180
|
def generate_image_path(ctx: Context, branch: str) -> str:
|
|
90
181
|
result = ctx.run('git remote get-url origin')
|
|
91
182
|
result = result.stdout.splitlines()[0]
|
|
@@ -123,7 +214,10 @@ def login(ctx: Context, username=None):
|
|
|
123
214
|
|
|
124
215
|
|
|
125
216
|
def commit_version(ctx: Context, version: Version) -> None:
|
|
126
|
-
|
|
217
|
+
if detect_project_type() == 'frontend':
|
|
218
|
+
ctx.run('git add package.json')
|
|
219
|
+
else:
|
|
220
|
+
ctx.run(f'git add {Version.VERSION_FILE_NAME}')
|
|
127
221
|
ctx.run('git add CHANGELOG.md')
|
|
128
222
|
ctx.run(f'git commit -m \'version update to {version}\'')
|
|
129
223
|
|
|
@@ -153,12 +247,14 @@ def pushimage(ctx: Context):
|
|
|
153
247
|
|
|
154
248
|
|
|
155
249
|
def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False) -> None:
|
|
250
|
+
project_type = detect_project_type()
|
|
156
251
|
if not skip_pipeline:
|
|
157
252
|
test(ctx)
|
|
158
253
|
analyze(ctx)
|
|
159
254
|
tag(ctx, version)
|
|
160
|
-
|
|
161
|
-
|
|
255
|
+
if project_type == 'backend':
|
|
256
|
+
buildimage(ctx)
|
|
257
|
+
pushimage(ctx)
|
|
162
258
|
changelog(ctx, version)
|
|
163
259
|
push(ctx)
|
|
164
260
|
|
|
@@ -169,19 +265,22 @@ def taginit(ctx: Context) -> None:
|
|
|
169
265
|
check_branch(ctx)
|
|
170
266
|
version = Version(0, 1, 0, 1)
|
|
171
267
|
tag(ctx, version)
|
|
172
|
-
version
|
|
268
|
+
save_version(version)
|
|
173
269
|
|
|
174
270
|
|
|
175
271
|
@task
|
|
176
272
|
def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
|
|
177
273
|
"""Increment release candidate version and deploy"""
|
|
178
274
|
check_branch(ctx)
|
|
179
|
-
|
|
180
|
-
|
|
275
|
+
project_type = detect_project_type()
|
|
276
|
+
if project_type == 'backend':
|
|
277
|
+
login(ctx)
|
|
278
|
+
version = read_version(ctx)
|
|
181
279
|
version.increment_release_candidate()
|
|
182
|
-
|
|
280
|
+
if project_type == 'backend':
|
|
281
|
+
lint(ctx, pylintrc=pylintrc)
|
|
183
282
|
tag_build_push(ctx, version, skip_pipeline)
|
|
184
|
-
version
|
|
283
|
+
save_version(version)
|
|
185
284
|
commit_version(ctx, version)
|
|
186
285
|
push_version(ctx)
|
|
187
286
|
|
|
@@ -190,10 +289,10 @@ def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
|
|
|
190
289
|
def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
|
|
191
290
|
"""Increment patch version, build, and deploy"""
|
|
192
291
|
production = check_branch(ctx)
|
|
193
|
-
version =
|
|
292
|
+
version = read_version(ctx)
|
|
194
293
|
version.increment_patch(release=production)
|
|
195
294
|
tag_build_push(ctx, version, skip_pipeline)
|
|
196
|
-
version
|
|
295
|
+
save_version(version)
|
|
197
296
|
commit_version(ctx, version)
|
|
198
297
|
push_version(ctx)
|
|
199
298
|
|
|
@@ -202,10 +301,10 @@ def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
|
|
|
202
301
|
def incrementminor(ctx: Context, skip_pipeline=False) -> None:
|
|
203
302
|
"""Increment minor version, build, and deploy"""
|
|
204
303
|
production = check_branch(ctx)
|
|
205
|
-
version =
|
|
304
|
+
version = read_version(ctx)
|
|
206
305
|
version.increment_minor(release=production)
|
|
207
306
|
tag_build_push(ctx, version, skip_pipeline)
|
|
208
|
-
version
|
|
307
|
+
save_version(version)
|
|
209
308
|
commit_version(ctx, version)
|
|
210
309
|
push_version(ctx)
|
|
211
310
|
|
|
@@ -214,10 +313,10 @@ def incrementminor(ctx: Context, skip_pipeline=False) -> None:
|
|
|
214
313
|
def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
|
|
215
314
|
"""Increment major version, build, and deploy"""
|
|
216
315
|
production = check_branch(ctx)
|
|
217
|
-
version =
|
|
316
|
+
version = read_version(ctx)
|
|
218
317
|
version.increment_major(release=production)
|
|
219
318
|
tag_build_push(ctx, version, skip_pipeline)
|
|
220
|
-
version
|
|
319
|
+
save_version(version)
|
|
221
320
|
commit_version(ctx, version)
|
|
222
321
|
push_version(ctx)
|
|
223
322
|
|
|
@@ -226,18 +325,22 @@ def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
|
|
|
226
325
|
def release(ctx: Context, skip_pipeline=False) -> None:
|
|
227
326
|
"""Promote release candidate to stable release and deploy"""
|
|
228
327
|
_ = check_branch(ctx)
|
|
229
|
-
|
|
230
|
-
|
|
328
|
+
if detect_project_type() == 'backend':
|
|
329
|
+
login(ctx)
|
|
330
|
+
version = read_version(ctx)
|
|
231
331
|
version.reset_release_candidate(release=True)
|
|
232
332
|
tag_build_push(ctx, version, skip_pipeline)
|
|
233
|
-
version
|
|
333
|
+
save_version(version)
|
|
234
334
|
commit_version(ctx, version)
|
|
235
335
|
push_version(ctx)
|
|
236
336
|
|
|
237
337
|
|
|
238
338
|
@task
|
|
239
339
|
def test(ctx: Context, settings='backend.test_settings'):
|
|
240
|
-
"""Run
|
|
340
|
+
"""Run tests with coverage (Django or Angular/karma)"""
|
|
341
|
+
if detect_project_type() == 'frontend':
|
|
342
|
+
ctx.run('npx ng test --no-watch --code-coverage --browsers=ChromeHeadlessNoSandbox')
|
|
343
|
+
return
|
|
241
344
|
detect_and_activate_venv()
|
|
242
345
|
ctx.run(f'python -m coverage run manage.py test --settings={settings} --noinput -v 2')
|
|
243
346
|
ctx.run('python -m coverage report -m')
|
|
@@ -246,7 +349,10 @@ def test(ctx: Context, settings='backend.test_settings'):
|
|
|
246
349
|
|
|
247
350
|
@task
|
|
248
351
|
def lint(ctx: Context, pylintrc=None):
|
|
249
|
-
"""Run pylint, generate reports for SonarQube"""
|
|
352
|
+
"""Run pylint, generate reports for SonarQube (backend only)"""
|
|
353
|
+
if detect_project_type() == 'frontend':
|
|
354
|
+
print("[itw] Skipping lint for frontend (SonarQube handles code quality)")
|
|
355
|
+
return
|
|
250
356
|
detect_and_activate_venv()
|
|
251
357
|
rcfile = pylintrc if pylintrc else PYLINTRC
|
|
252
358
|
print("\n" + "=" * 60)
|
|
@@ -265,7 +371,10 @@ def lint(ctx: Context, pylintrc=None):
|
|
|
265
371
|
|
|
266
372
|
@task
|
|
267
373
|
def lintlocal(ctx: Context, pylintrc=None):
|
|
268
|
-
"""Run pylint locally with human-readable output (
|
|
374
|
+
"""Run pylint locally with human-readable output (backend only)"""
|
|
375
|
+
if detect_project_type() == 'frontend':
|
|
376
|
+
print("[itw] Skipping lint for frontend (SonarQube handles code quality)")
|
|
377
|
+
return
|
|
269
378
|
detect_and_activate_venv()
|
|
270
379
|
rcfile = pylintrc if pylintrc else PYLINTRC
|
|
271
380
|
print("\n" + "=" * 60)
|
|
@@ -277,10 +386,11 @@ def lintlocal(ctx: Context, pylintrc=None):
|
|
|
277
386
|
@task
|
|
278
387
|
def analyze(ctx: Context):
|
|
279
388
|
"""Run SonarQube analysis"""
|
|
280
|
-
|
|
281
|
-
|
|
389
|
+
if detect_project_type() == 'backend':
|
|
390
|
+
detect_and_activate_venv()
|
|
391
|
+
load_env(ctx)
|
|
282
392
|
try:
|
|
283
|
-
version =
|
|
393
|
+
version = read_version(ctx)
|
|
284
394
|
sonar_version = str(version).lstrip('v.')
|
|
285
395
|
except:
|
|
286
396
|
sonar_version = "0.0.0"
|
|
@@ -297,7 +407,11 @@ def analyze(ctx: Context):
|
|
|
297
407
|
|
|
298
408
|
@task
|
|
299
409
|
def buildlocal(ctx: Context):
|
|
300
|
-
"""Build Docker image
|
|
410
|
+
"""Build locally (Docker image for backend, npm build for frontend)"""
|
|
411
|
+
if detect_project_type() == 'frontend':
|
|
412
|
+
ctx.run('npm run build')
|
|
413
|
+
print('✓ Built frontend (dist/)')
|
|
414
|
+
return
|
|
301
415
|
current_branch = get_current_branch(ctx)
|
|
302
416
|
local_tag = f'myapp:{current_branch}-local'
|
|
303
417
|
ctx.run(f'podman build . --tag={local_tag}')
|
|
@@ -306,7 +420,11 @@ def buildlocal(ctx: Context):
|
|
|
306
420
|
|
|
307
421
|
@task
|
|
308
422
|
def testlocal(ctx: Context, settings='backend.test_settings'):
|
|
309
|
-
"""Run tests locally (
|
|
423
|
+
"""Run tests locally (Django or Angular/karma)"""
|
|
424
|
+
if detect_project_type() == 'frontend':
|
|
425
|
+
ctx.run('npx ng test --no-watch --code-coverage --browsers=ChromeHeadlessNoSandbox')
|
|
426
|
+
print("✓ Tests completed")
|
|
427
|
+
return
|
|
310
428
|
detect_and_activate_venv()
|
|
311
429
|
ctx.run(f'python -m coverage run manage.py test --settings={settings} --noinput -v 2')
|
|
312
430
|
ctx.run('python -m coverage report -m')
|
|
@@ -317,11 +435,12 @@ def testlocal(ctx: Context, settings='backend.test_settings'):
|
|
|
317
435
|
@task
|
|
318
436
|
def analyzelocal(ctx: Context):
|
|
319
437
|
"""Run SonarQube static analysis locally"""
|
|
320
|
-
|
|
321
|
-
|
|
438
|
+
if detect_project_type() == 'backend':
|
|
439
|
+
detect_and_activate_venv()
|
|
440
|
+
load_env(ctx)
|
|
322
441
|
print("Running SonarQube analysis...")
|
|
323
442
|
try:
|
|
324
|
-
version =
|
|
443
|
+
version = read_version(ctx)
|
|
325
444
|
sonar_version = str(version).lstrip('v.')
|
|
326
445
|
except:
|
|
327
446
|
sonar_version = "0.0.0"
|
|
@@ -340,20 +459,22 @@ def analyzelocal(ctx: Context):
|
|
|
340
459
|
|
|
341
460
|
@task
|
|
342
461
|
def pipelinelocal(ctx: Context, settings='backend.test_settings', pylintrc=None):
|
|
343
|
-
"""Local pipeline: lint → test → analyze
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
462
|
+
"""Local pipeline: (lint →) test → analyze"""
|
|
463
|
+
project_type = detect_project_type()
|
|
464
|
+
if project_type == 'backend':
|
|
465
|
+
detect_and_activate_venv()
|
|
466
|
+
print("=" * 60)
|
|
467
|
+
print("Step 1: Running lint...")
|
|
468
|
+
print("=" * 60)
|
|
469
|
+
lint(ctx, pylintrc=pylintrc)
|
|
349
470
|
|
|
350
471
|
print("\n" + "=" * 60)
|
|
351
|
-
print("Step 2: Running tests...")
|
|
472
|
+
print(f"Step {'2' if project_type == 'backend' else '1'}: Running tests...")
|
|
352
473
|
print("=" * 60)
|
|
353
474
|
testlocal(ctx, settings)
|
|
354
475
|
|
|
355
476
|
print("\n" + "=" * 60)
|
|
356
|
-
print("Step 3: Running SonarQube analysis...")
|
|
477
|
+
print(f"Step {'3' if project_type == 'backend' else '2'}: Running SonarQube analysis...")
|
|
357
478
|
print("=" * 60)
|
|
358
479
|
analyzelocal(ctx)
|
|
359
480
|
|
|
@@ -431,7 +552,7 @@ def changelog(ctx: Context, version: Version = None):
|
|
|
431
552
|
version_str = str(version)
|
|
432
553
|
else:
|
|
433
554
|
try:
|
|
434
|
-
version_str = str(
|
|
555
|
+
version_str = str(read_version(ctx))
|
|
435
556
|
except:
|
|
436
557
|
version_str = "Unreleased"
|
|
437
558
|
|
|
@@ -76,6 +76,10 @@ class Version:
|
|
|
76
76
|
else:
|
|
77
77
|
raise ValueError(f'Invalid version: {version}')
|
|
78
78
|
|
|
79
|
+
def bare_semver(self) -> str:
|
|
80
|
+
"""Return just major.minor.patch (e.g. '2.0.4') for package.json."""
|
|
81
|
+
return f'{self.major}.{self.minor}.{self.patch}'
|
|
82
|
+
|
|
79
83
|
def __str__(self):
|
|
80
84
|
version = f'v.{self.major}.{self.minor}.{self.patch}'
|
|
81
85
|
if self.release_candidate == 0:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "itw_python_builder"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.25"
|
|
8
8
|
description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/requires.txt
RENAMED
|
File without changes
|
{itw_python_builder-0.1.23 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|