itw-python-builder 0.1.24__tar.gz → 0.1.26__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.26}/PKG-INFO +1 -1
  2. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder/tasks.py +182 -42
  3. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder/version.py +4 -0
  4. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder.egg-info/PKG-INFO +1 -1
  5. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/pyproject.toml +1 -1
  6. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/LICENSE +0 -0
  7. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/README.md +0 -0
  8. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder/.pylintrc +0 -0
  9. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder/__init__.py +0 -0
  10. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder/cli.py +0 -0
  11. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder.egg-info/SOURCES.txt +0 -0
  12. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder.egg-info/dependency_links.txt +0 -0
  13. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder.egg-info/entry_points.txt +0 -0
  14. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder.egg-info/requires.txt +0 -0
  15. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/itw_python_builder.egg-info/top_level.txt +0 -0
  16. {itw_python_builder-0.1.24 → itw_python_builder-0.1.26}/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.26
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,27 @@ 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
+ if detect_project_type() == 'frontend':
92
+ env_dir = os.path.join(os.getcwd(), 'src', 'environments')
93
+ candidates = []
94
+ if ctx is not None:
95
+ branch = get_current_branch(ctx)
96
+ candidates.append(os.path.join(env_dir, f'environment.{branch}.ts'))
97
+ candidates.append(os.path.join(env_dir, 'environment.ts'))
98
+ for path in candidates:
99
+ if os.path.exists(path):
100
+ for key, value in _parse_angular_env_file(path).items():
101
+ os.environ.setdefault(key, value)
102
+ return
83
103
  from decouple import Config, RepositoryEnv
84
104
  env_path = os.path.join(os.getcwd(), '.env')
85
105
  if not os.path.exists(env_path):
@@ -122,6 +142,33 @@ def get_latest_tag(ctx: Context) -> Version:
122
142
  return Version.parse(latest_tag)
123
143
 
124
144
 
145
+ def _update_package_json_version(version: Version) -> None:
146
+ """Update the top-level `version` in package.json (preserves key order)."""
147
+ import json
148
+ pkg_path = os.path.join(os.getcwd(), 'package.json')
149
+ with open(pkg_path, 'r', encoding='utf-8') as fp:
150
+ data = json.load(fp)
151
+ data['version'] = version.bare_semver()
152
+ with open(pkg_path, 'w', encoding='utf-8') as fp:
153
+ json.dump(data, fp, indent=2)
154
+ fp.write('\n')
155
+
156
+
157
+ def read_version(ctx: Context) -> Version:
158
+ """Read current version. Backend → VERSION file. Frontend → latest git tag."""
159
+ if detect_project_type() == 'frontend':
160
+ return get_latest_tag(ctx)
161
+ return Version.read_from_file()
162
+
163
+
164
+ def save_version(version: Version) -> None:
165
+ """Persist version. Backend → VERSION file. Frontend → package.json (bare semver)."""
166
+ if detect_project_type() == 'frontend':
167
+ _update_package_json_version(version)
168
+ return
169
+ version.save_to_file()
170
+
171
+
125
172
  def generate_image_path(ctx: Context, branch: str) -> str:
126
173
  result = ctx.run('git remote get-url origin')
127
174
  result = result.stdout.splitlines()[0]
@@ -145,21 +192,83 @@ def get_gitlab_username(ctx: Context) -> str:
145
192
 
146
193
  @task
147
194
  def login(ctx: Context, username=None):
148
- """Log in to the GitLab container registry."""
195
+ """Capture GitLab token. Backend also logs into the container registry."""
149
196
  if not username:
150
197
  username = get_gitlab_username(ctx)
151
- print(f'Logging in to gitreg.it-works.io:443 as {username}')
152
- token = getpass.getpass(f'Enter GitLab token (password) for {username}: ')
198
+ token = getpass.getpass(f'Enter GitLab token for {username}: ')
153
199
  if not token:
154
200
  raise RuntimeError('No token entered; aborting login.')
201
+ os.environ['GITLAB_TOKEN'] = token
202
+ os.environ['GITLAB_USERNAME'] = username
203
+ if detect_project_type() == 'backend':
204
+ print(f'Logging in to gitreg.it-works.io:443 as {username}')
205
+ ctx.run(
206
+ f'podman login -u {username} --password-stdin gitreg.it-works.io:443 --tls-verify=false',
207
+ in_stream=io.StringIO(token)
208
+ )
209
+ else:
210
+ print(f'[itw] GitLab token captured for {username} (will be used for package upload).')
211
+
212
+
213
+ def _read_package_name() -> str:
214
+ """Read 'name' field from package.json in cwd."""
215
+ import json
216
+ pkg_path = os.path.join(os.getcwd(), 'package.json')
217
+ with open(pkg_path, 'r', encoding='utf-8') as fp:
218
+ return json.load(fp)['name']
219
+
220
+
221
+ def _parse_gitlab_remote(ctx: Context) -> tuple:
222
+ """Return (host, project_path) parsed from `git remote get-url origin`."""
223
+ result = ctx.run('git remote get-url origin', hide=True)
224
+ url = result.stdout.strip()
225
+ if url.endswith('.git'):
226
+ url = url[:-4]
227
+ if url.startswith('git@'):
228
+ host, path = url.split('@', 1)[1].split(':', 1)
229
+ elif '://' in url:
230
+ host, path = url.split('://', 1)[1].split('/', 1)
231
+ else:
232
+ raise RuntimeError(f'Cannot parse GitLab remote URL: {url}')
233
+ return host, path
234
+
235
+
236
+ def build_frontend(ctx: Context, branch: str) -> None:
237
+ """Build the Angular app. Master branch → production config; otherwise staging."""
238
+ config = 'production' if branch == 'master' else 'staging'
239
+ ctx.run(f'npx ng build --configuration={config}')
240
+
241
+
242
+ def package_dist(ctx: Context) -> None:
243
+ """Tar the dist/ output directory into dist.tar.gz."""
244
+ if not os.path.isdir(os.path.join(os.getcwd(), 'dist')):
245
+ raise RuntimeError('No dist/ directory found after build — did `ng build` succeed?')
246
+ ctx.run('tar -czf dist.tar.gz -C dist .')
247
+
248
+
249
+ def upload_dist(ctx: Context, version) -> None:
250
+ """Upload dist.tar.gz to this project's GitLab Generic Package Registry under <version>/."""
251
+ from urllib.parse import quote
252
+ if not os.environ.get('GITLAB_TOKEN'):
253
+ raise RuntimeError('GITLAB_TOKEN not set; run `itw login` first.')
254
+ host, path = _parse_gitlab_remote(ctx)
255
+ encoded = quote(path, safe='')
256
+ url = f'https://{host}/api/v4/projects/{encoded}/packages/generic/frontend/{version}/dist.tar.gz'
257
+ print(f'[itw] Uploading dist.tar.gz → {url}')
155
258
  ctx.run(
156
- f'podman login -u {username} --password-stdin gitreg.it-works.io:443 --tls-verify=false',
157
- in_stream=io.StringIO(token)
259
+ 'curl --fail --silent --show-error '
260
+ '--header "PRIVATE-TOKEN: $GITLAB_TOKEN" '
261
+ '--upload-file dist.tar.gz '
262
+ f'"{url}"'
158
263
  )
264
+ print('[itw] Upload complete.')
159
265
 
160
266
 
161
267
  def commit_version(ctx: Context, version: Version) -> None:
162
- ctx.run(f'git add {Version.VERSION_FILE_NAME}')
268
+ if detect_project_type() == 'frontend':
269
+ ctx.run('git add package.json')
270
+ else:
271
+ ctx.run(f'git add {Version.VERSION_FILE_NAME}')
163
272
  ctx.run('git add CHANGELOG.md')
164
273
  ctx.run(f'git commit -m \'version update to {version}\'')
165
274
 
@@ -189,12 +298,19 @@ def pushimage(ctx: Context):
189
298
 
190
299
 
191
300
  def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False) -> None:
301
+ project_type = detect_project_type()
192
302
  if not skip_pipeline:
193
303
  test(ctx)
194
304
  analyze(ctx)
195
305
  tag(ctx, version)
196
- buildimage(ctx)
197
- pushimage(ctx)
306
+ if project_type == 'backend':
307
+ buildimage(ctx)
308
+ pushimage(ctx)
309
+ else:
310
+ branch = get_current_branch(ctx)
311
+ build_frontend(ctx, branch)
312
+ package_dist(ctx)
313
+ upload_dist(ctx, version)
198
314
  changelog(ctx, version)
199
315
  push(ctx)
200
316
 
@@ -205,7 +321,7 @@ def taginit(ctx: Context) -> None:
205
321
  check_branch(ctx)
206
322
  version = Version(0, 1, 0, 1)
207
323
  tag(ctx, version)
208
- version.save_to_file()
324
+ save_version(version)
209
325
 
210
326
 
211
327
  @task
@@ -213,11 +329,13 @@ def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
213
329
  """Increment release candidate version and deploy"""
214
330
  check_branch(ctx)
215
331
  login(ctx)
216
- version = Version.read_from_file()
332
+ project_type = detect_project_type()
333
+ version = read_version(ctx)
217
334
  version.increment_release_candidate()
218
- lint(ctx, pylintrc=pylintrc)
335
+ if project_type == 'backend':
336
+ lint(ctx, pylintrc=pylintrc)
219
337
  tag_build_push(ctx, version, skip_pipeline)
220
- version.save_to_file()
338
+ save_version(version)
221
339
  commit_version(ctx, version)
222
340
  push_version(ctx)
223
341
 
@@ -226,10 +344,10 @@ def incrementrc(ctx: Context, skip_pipeline=False, pylintrc=None) -> None:
226
344
  def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
227
345
  """Increment patch version, build, and deploy"""
228
346
  production = check_branch(ctx)
229
- version = Version.read_from_file()
347
+ version = read_version(ctx)
230
348
  version.increment_patch(release=production)
231
349
  tag_build_push(ctx, version, skip_pipeline)
232
- version.save_to_file()
350
+ save_version(version)
233
351
  commit_version(ctx, version)
234
352
  push_version(ctx)
235
353
 
@@ -238,10 +356,10 @@ def incrementpatch(ctx: Context, skip_pipeline=False) -> None:
238
356
  def incrementminor(ctx: Context, skip_pipeline=False) -> None:
239
357
  """Increment minor version, build, and deploy"""
240
358
  production = check_branch(ctx)
241
- version = Version.read_from_file()
359
+ version = read_version(ctx)
242
360
  version.increment_minor(release=production)
243
361
  tag_build_push(ctx, version, skip_pipeline)
244
- version.save_to_file()
362
+ save_version(version)
245
363
  commit_version(ctx, version)
246
364
  push_version(ctx)
247
365
 
@@ -250,10 +368,10 @@ def incrementminor(ctx: Context, skip_pipeline=False) -> None:
250
368
  def incrementmajor(ctx: Context, skip_pipeline=False) -> None:
251
369
  """Increment major version, build, and deploy"""
252
370
  production = check_branch(ctx)
253
- version = Version.read_from_file()
371
+ version = read_version(ctx)
254
372
  version.increment_major(release=production)
255
373
  tag_build_push(ctx, version, skip_pipeline)
256
- version.save_to_file()
374
+ save_version(version)
257
375
  commit_version(ctx, version)
258
376
  push_version(ctx)
259
377
 
@@ -263,17 +381,20 @@ def release(ctx: Context, skip_pipeline=False) -> None:
263
381
  """Promote release candidate to stable release and deploy"""
264
382
  _ = check_branch(ctx)
265
383
  login(ctx)
266
- version = Version.read_from_file()
384
+ version = read_version(ctx)
267
385
  version.reset_release_candidate(release=True)
268
386
  tag_build_push(ctx, version, skip_pipeline)
269
- version.save_to_file()
387
+ save_version(version)
270
388
  commit_version(ctx, version)
271
389
  push_version(ctx)
272
390
 
273
391
 
274
392
  @task
275
393
  def test(ctx: Context, settings='backend.test_settings'):
276
- """Run Django tests with coverage"""
394
+ """Run tests with coverage (Django or Angular/karma)"""
395
+ if detect_project_type() == 'frontend':
396
+ ctx.run('npx ng test --no-watch --code-coverage --browsers=ChromeHeadlessNoSandbox')
397
+ return
277
398
  detect_and_activate_venv()
278
399
  ctx.run(f'python -m coverage run manage.py test --settings={settings} --noinput -v 2')
279
400
  ctx.run('python -m coverage report -m')
@@ -282,7 +403,10 @@ def test(ctx: Context, settings='backend.test_settings'):
282
403
 
283
404
  @task
284
405
  def lint(ctx: Context, pylintrc=None):
285
- """Run pylint, generate reports for SonarQube"""
406
+ """Run pylint, generate reports for SonarQube (backend only)"""
407
+ if detect_project_type() == 'frontend':
408
+ print("[itw] Skipping lint for frontend (SonarQube handles code quality)")
409
+ return
286
410
  detect_and_activate_venv()
287
411
  rcfile = pylintrc if pylintrc else PYLINTRC
288
412
  print("\n" + "=" * 60)
@@ -301,7 +425,10 @@ def lint(ctx: Context, pylintrc=None):
301
425
 
302
426
  @task
303
427
  def lintlocal(ctx: Context, pylintrc=None):
304
- """Run pylint locally with human-readable output (no report files)"""
428
+ """Run pylint locally with human-readable output (backend only)"""
429
+ if detect_project_type() == 'frontend':
430
+ print("[itw] Skipping lint for frontend (SonarQube handles code quality)")
431
+ return
305
432
  detect_and_activate_venv()
306
433
  rcfile = pylintrc if pylintrc else PYLINTRC
307
434
  print("\n" + "=" * 60)
@@ -313,10 +440,11 @@ def lintlocal(ctx: Context, pylintrc=None):
313
440
  @task
314
441
  def analyze(ctx: Context):
315
442
  """Run SonarQube analysis"""
316
- detect_and_activate_venv()
317
- load_env()
443
+ if detect_project_type() == 'backend':
444
+ detect_and_activate_venv()
445
+ load_env(ctx)
318
446
  try:
319
- version = Version.read_from_file()
447
+ version = read_version(ctx)
320
448
  sonar_version = str(version).lstrip('v.')
321
449
  except:
322
450
  sonar_version = "0.0.0"
@@ -333,7 +461,12 @@ def analyze(ctx: Context):
333
461
 
334
462
  @task
335
463
  def buildlocal(ctx: Context):
336
- """Build Docker image locally without pushing to registry"""
464
+ """Build locally (Docker image for backend, ng build for frontend)"""
465
+ if detect_project_type() == 'frontend':
466
+ branch = get_current_branch(ctx)
467
+ build_frontend(ctx, branch)
468
+ print('✓ Built frontend (dist/)')
469
+ return
337
470
  current_branch = get_current_branch(ctx)
338
471
  local_tag = f'myapp:{current_branch}-local'
339
472
  ctx.run(f'podman build . --tag={local_tag}')
@@ -342,7 +475,11 @@ def buildlocal(ctx: Context):
342
475
 
343
476
  @task
344
477
  def testlocal(ctx: Context, settings='backend.test_settings'):
345
- """Run tests locally (not in Docker)"""
478
+ """Run tests locally (Django or Angular/karma)"""
479
+ if detect_project_type() == 'frontend':
480
+ ctx.run('npx ng test --no-watch --code-coverage --browsers=ChromeHeadlessNoSandbox')
481
+ print("✓ Tests completed")
482
+ return
346
483
  detect_and_activate_venv()
347
484
  ctx.run(f'python -m coverage run manage.py test --settings={settings} --noinput -v 2')
348
485
  ctx.run('python -m coverage report -m')
@@ -353,11 +490,12 @@ def testlocal(ctx: Context, settings='backend.test_settings'):
353
490
  @task
354
491
  def analyzelocal(ctx: Context):
355
492
  """Run SonarQube static analysis locally"""
356
- detect_and_activate_venv()
357
- load_env()
493
+ if detect_project_type() == 'backend':
494
+ detect_and_activate_venv()
495
+ load_env(ctx)
358
496
  print("Running SonarQube analysis...")
359
497
  try:
360
- version = Version.read_from_file()
498
+ version = read_version(ctx)
361
499
  sonar_version = str(version).lstrip('v.')
362
500
  except:
363
501
  sonar_version = "0.0.0"
@@ -376,20 +514,22 @@ def analyzelocal(ctx: Context):
376
514
 
377
515
  @task
378
516
  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)
517
+ """Local pipeline: (lint →) test → analyze"""
518
+ project_type = detect_project_type()
519
+ if project_type == 'backend':
520
+ detect_and_activate_venv()
521
+ print("=" * 60)
522
+ print("Step 1: Running lint...")
523
+ print("=" * 60)
524
+ lint(ctx, pylintrc=pylintrc)
385
525
 
386
526
  print("\n" + "=" * 60)
387
- print("Step 2: Running tests...")
527
+ print(f"Step {'2' if project_type == 'backend' else '1'}: Running tests...")
388
528
  print("=" * 60)
389
529
  testlocal(ctx, settings)
390
530
 
391
531
  print("\n" + "=" * 60)
392
- print("Step 3: Running SonarQube analysis...")
532
+ print(f"Step {'3' if project_type == 'backend' else '2'}: Running SonarQube analysis...")
393
533
  print("=" * 60)
394
534
  analyzelocal(ctx)
395
535
 
@@ -467,7 +607,7 @@ def changelog(ctx: Context, version: Version = None):
467
607
  version_str = str(version)
468
608
  else:
469
609
  try:
470
- version_str = str(Version.read_from_file())
610
+ version_str = str(read_version(ctx))
471
611
  except:
472
612
  version_str = "Unreleased"
473
613
 
@@ -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.26
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.26"
8
8
  description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"