itw-python-builder 0.1.24__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.
Files changed (16) hide show
  1. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/PKG-INFO +1 -1
  2. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder/tasks.py +124 -39
  3. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder/version.py +4 -0
  4. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/PKG-INFO +1 -1
  5. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/pyproject.toml +1 -1
  6. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/LICENSE +0 -0
  7. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/README.md +0 -0
  8. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder/.pylintrc +0 -0
  9. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder/__init__.py +0 -0
  10. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder/cli.py +0 -0
  11. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/SOURCES.txt +0 -0
  12. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/dependency_links.txt +0 -0
  13. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/entry_points.txt +0 -0
  14. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/requires.txt +0 -0
  15. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/itw_python_builder.egg-info/top_level.txt +0 -0
  16. {itw_python_builder-0.1.24 → itw_python_builder-0.1.25}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itw_python_builder
3
- Version: 0.1.24
3
+ Version: 0.1.25
4
4
  Summary: Standardized Django deployment pipeline with Docker, testing, and SonarQube integration
5
5
  Author-email: IT-Works <contact@it-works.io>
6
6
  License: MIT
@@ -3,6 +3,7 @@ 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
 
@@ -78,8 +79,29 @@ def detect_and_activate_venv():
78
79
  print(f"[itw] Using venv: {venv_path}")
79
80
  _venv_activated = True
80
81
 
81
- def load_env():
82
- """Load .env file from current directory into os.environ."""
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
83
105
  from decouple import Config, RepositoryEnv
84
106
  env_path = os.path.join(os.getcwd(), '.env')
85
107
  if not os.path.exists(env_path):
@@ -122,6 +144,39 @@ def get_latest_tag(ctx: Context) -> Version:
122
144
  return Version.parse(latest_tag)
123
145
 
124
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
+
125
180
  def generate_image_path(ctx: Context, branch: str) -> str:
126
181
  result = ctx.run('git remote get-url origin')
127
182
  result = result.stdout.splitlines()[0]
@@ -159,7 +214,10 @@ def login(ctx: Context, username=None):
159
214
 
160
215
 
161
216
  def commit_version(ctx: Context, version: Version) -> None:
162
- ctx.run(f'git add {Version.VERSION_FILE_NAME}')
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}')
163
221
  ctx.run('git add CHANGELOG.md')
164
222
  ctx.run(f'git commit -m \'version update to {version}\'')
165
223
 
@@ -189,12 +247,14 @@ def pushimage(ctx: Context):
189
247
 
190
248
 
191
249
  def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False) -> None:
250
+ project_type = detect_project_type()
192
251
  if not skip_pipeline:
193
252
  test(ctx)
194
253
  analyze(ctx)
195
254
  tag(ctx, version)
196
- buildimage(ctx)
197
- pushimage(ctx)
255
+ if project_type == 'backend':
256
+ buildimage(ctx)
257
+ pushimage(ctx)
198
258
  changelog(ctx, version)
199
259
  push(ctx)
200
260
 
@@ -205,19 +265,22 @@ def taginit(ctx: Context) -> None:
205
265
  check_branch(ctx)
206
266
  version = Version(0, 1, 0, 1)
207
267
  tag(ctx, version)
208
- version.save_to_file()
268
+ save_version(version)
209
269
 
210
270
 
211
271
  @task
212
272
  def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
213
273
  """Increment release candidate version and deploy"""
214
274
  check_branch(ctx)
215
- login(ctx)
216
- version = Version.read_from_file()
275
+ project_type = detect_project_type()
276
+ if project_type == 'backend':
277
+ login(ctx)
278
+ version = read_version(ctx)
217
279
  version.increment_release_candidate()
218
- lint(ctx, pylintrc=pylintrc)
280
+ if project_type == 'backend':
281
+ lint(ctx, pylintrc=pylintrc)
219
282
  tag_build_push(ctx, version, skip_pipeline)
220
- version.save_to_file()
283
+ save_version(version)
221
284
  commit_version(ctx, version)
222
285
  push_version(ctx)
223
286
 
@@ -226,10 +289,10 @@ def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
226
289
  def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
227
290
  """Increment patch version, build, and deploy"""
228
291
  production = check_branch(ctx)
229
- version = Version.read_from_file()
292
+ version = read_version(ctx)
230
293
  version.increment_patch(release=production)
231
294
  tag_build_push(ctx, version, skip_pipeline)
232
- version.save_to_file()
295
+ save_version(version)
233
296
  commit_version(ctx, version)
234
297
  push_version(ctx)
235
298
 
@@ -238,10 +301,10 @@ def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
238
301
  def incrementminor(ctx: Context, skip_pipeline=False) -> None:
239
302
  """Increment minor version, build, and deploy"""
240
303
  production = check_branch(ctx)
241
- version = Version.read_from_file()
304
+ version = read_version(ctx)
242
305
  version.increment_minor(release=production)
243
306
  tag_build_push(ctx, version, skip_pipeline)
244
- version.save_to_file()
307
+ save_version(version)
245
308
  commit_version(ctx, version)
246
309
  push_version(ctx)
247
310
 
@@ -250,10 +313,10 @@ def incrementminor(ctx: Context, skip_pipeline=False) -> None:
250
313
  def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
251
314
  """Increment major version, build, and deploy"""
252
315
  production = check_branch(ctx)
253
- version = Version.read_from_file()
316
+ version = read_version(ctx)
254
317
  version.increment_major(release=production)
255
318
  tag_build_push(ctx, version, skip_pipeline)
256
- version.save_to_file()
319
+ save_version(version)
257
320
  commit_version(ctx, version)
258
321
  push_version(ctx)
259
322
 
@@ -262,18 +325,22 @@ def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
262
325
  def release(ctx: Context, skip_pipeline=False) -> None:
263
326
  """Promote release candidate to stable release and deploy"""
264
327
  _ = check_branch(ctx)
265
- login(ctx)
266
- version = Version.read_from_file()
328
+ if detect_project_type() == 'backend':
329
+ login(ctx)
330
+ version = read_version(ctx)
267
331
  version.reset_release_candidate(release=True)
268
332
  tag_build_push(ctx, version, skip_pipeline)
269
- version.save_to_file()
333
+ save_version(version)
270
334
  commit_version(ctx, version)
271
335
  push_version(ctx)
272
336
 
273
337
 
274
338
  @task
275
339
  def test(ctx: Context, settings='backend.test_settings'):
276
- """Run Django tests with coverage"""
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
277
344
  detect_and_activate_venv()
278
345
  ctx.run(f'python -m coverage run manage.py test --settings={settings} --noinput -v 2')
279
346
  ctx.run('python -m coverage report -m')
@@ -282,7 +349,10 @@ def test(ctx: Context, settings='backend.test_settings'):
282
349
 
283
350
  @task
284
351
  def lint(ctx: Context, pylintrc=None):
285
- """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
286
356
  detect_and_activate_venv()
287
357
  rcfile = pylintrc if pylintrc else PYLINTRC
288
358
  print("\n" + "=" * 60)
@@ -301,7 +371,10 @@ def lint(ctx: Context, pylintrc=None):
301
371
 
302
372
  @task
303
373
  def lintlocal(ctx: Context, pylintrc=None):
304
- """Run pylint locally with human-readable output (no report files)"""
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
305
378
  detect_and_activate_venv()
306
379
  rcfile = pylintrc if pylintrc else PYLINTRC
307
380
  print("\n" + "=" * 60)
@@ -313,10 +386,11 @@ def lintlocal(ctx: Context, pylintrc=None):
313
386
  @task
314
387
  def analyze(ctx: Context):
315
388
  """Run SonarQube analysis"""
316
- detect_and_activate_venv()
317
- load_env()
389
+ if detect_project_type() == 'backend':
390
+ detect_and_activate_venv()
391
+ load_env(ctx)
318
392
  try:
319
- version = Version.read_from_file()
393
+ version = read_version(ctx)
320
394
  sonar_version = str(version).lstrip('v.')
321
395
  except:
322
396
  sonar_version = "0.0.0"
@@ -333,7 +407,11 @@ def analyze(ctx: Context):
333
407
 
334
408
  @task
335
409
  def buildlocal(ctx: Context):
336
- """Build Docker image locally without pushing to registry"""
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
337
415
  current_branch = get_current_branch(ctx)
338
416
  local_tag = f'myapp:{current_branch}-local'
339
417
  ctx.run(f'podman build . --tag={local_tag}')
@@ -342,7 +420,11 @@ def buildlocal(ctx: Context):
342
420
 
343
421
  @task
344
422
  def testlocal(ctx: Context, settings='backend.test_settings'):
345
- """Run tests locally (not in Docker)"""
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
346
428
  detect_and_activate_venv()
347
429
  ctx.run(f'python -m coverage run manage.py test --settings={settings} --noinput -v 2')
348
430
  ctx.run('python -m coverage report -m')
@@ -353,11 +435,12 @@ def testlocal(ctx: Context, settings='backend.test_settings'):
353
435
  @task
354
436
  def analyzelocal(ctx: Context):
355
437
  """Run SonarQube static analysis locally"""
356
- detect_and_activate_venv()
357
- load_env()
438
+ if detect_project_type() == 'backend':
439
+ detect_and_activate_venv()
440
+ load_env(ctx)
358
441
  print("Running SonarQube analysis...")
359
442
  try:
360
- version = Version.read_from_file()
443
+ version = read_version(ctx)
361
444
  sonar_version = str(version).lstrip('v.')
362
445
  except:
363
446
  sonar_version = "0.0.0"
@@ -376,20 +459,22 @@ def analyzelocal(ctx: Context):
376
459
 
377
460
  @task
378
461
  def pipelinelocal(ctx: Context, settings='backend.test_settings', pylintrc=None):
379
- """Local pipeline: lint → test → analyze → build"""
380
- detect_and_activate_venv()
381
- print("=" * 60)
382
- print("Step 1: Running lint...")
383
- print("=" * 60)
384
- lint(ctx, pylintrc=pylintrc)
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)
385
470
 
386
471
  print("\n" + "=" * 60)
387
- print("Step 2: Running tests...")
472
+ print(f"Step {'2' if project_type == 'backend' else '1'}: Running tests...")
388
473
  print("=" * 60)
389
474
  testlocal(ctx, settings)
390
475
 
391
476
  print("\n" + "=" * 60)
392
- print("Step 3: Running SonarQube analysis...")
477
+ print(f"Step {'3' if project_type == 'backend' else '2'}: Running SonarQube analysis...")
393
478
  print("=" * 60)
394
479
  analyzelocal(ctx)
395
480
 
@@ -467,7 +552,7 @@ def changelog(ctx: Context, version: Version = None):
467
552
  version_str = str(version)
468
553
  else:
469
554
  try:
470
- version_str = str(Version.read_from_file())
555
+ version_str = str(read_version(ctx))
471
556
  except:
472
557
  version_str = "Unreleased"
473
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itw_python_builder
3
- Version: 0.1.24
3
+ Version: 0.1.25
4
4
  Summary: Standardized Django deployment pipeline with Docker, testing, and SonarQube integration
5
5
  Author-email: IT-Works <contact@it-works.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "itw_python_builder"
7
- version = "0.1.24"
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"