itw-python-builder 0.2.0__tar.gz → 0.2.2__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 (22) hide show
  1. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/PKG-INFO +1 -1
  2. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/tasks.py +65 -1
  3. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/templates/new_version_email.html +2 -1
  4. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/utils.py +135 -0
  5. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder.egg-info/PKG-INFO +1 -1
  6. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/pyproject.toml +1 -1
  7. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/LICENSE +0 -0
  8. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/README.md +0 -0
  9. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/.pylintrc +0 -0
  10. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/__init__.py +0 -0
  11. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/cli.py +0 -0
  12. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/notify.py +0 -0
  13. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/ssr_tasks.py +0 -0
  14. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/templates/server.sitemap.snippet.ts +0 -0
  15. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/templates/sitemap.routes.ts +0 -0
  16. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder/version.py +0 -0
  17. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder.egg-info/SOURCES.txt +0 -0
  18. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder.egg-info/dependency_links.txt +0 -0
  19. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder.egg-info/entry_points.txt +0 -0
  20. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder.egg-info/requires.txt +0 -0
  21. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/itw_python_builder.egg-info/top_level.txt +0 -0
  22. {itw_python_builder-0.2.0 → itw_python_builder-0.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itw_python_builder
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -1,6 +1,7 @@
1
1
  import getpass
2
2
  import io
3
3
  import shutil
4
+ import sys
4
5
  import tempfile
5
6
  from datetime import datetime
6
7
 
@@ -26,6 +27,10 @@ from itw_python_builder.utils import (
26
27
  probe_gitlab_token,
27
28
  TOKEN_CACHE_PATH,
28
29
  maybe_override_release_version,
30
+ ensure_gitlab_token,
31
+ resolve_pm_repo,
32
+ create_gitlab_task,
33
+ parse_md_issues,
29
34
  )
30
35
  from itw_python_builder.version import Version
31
36
 
@@ -106,6 +111,65 @@ def logout(ctx: Context):
106
111
  os.environ.pop('GITLAB_USERNAME', None)
107
112
 
108
113
 
114
+ @task(name='task')
115
+ def create_task(ctx: Context, file=None):
116
+ """Create GitLab tasks in the group's _pm repo from a markdown file (--file path.md)."""
117
+ if not file:
118
+ raise RuntimeError('Missing --file path to markdown file.')
119
+ if not file.lower().endswith('.md'):
120
+ raise RuntimeError(f'File must be .md, got {file!r}.')
121
+ if not os.path.isfile(file):
122
+ raise RuntimeError(f'File not found: {file}')
123
+ if not os.path.isdir(os.path.join(os.getcwd(), '.git')):
124
+ raise RuntimeError('Not a git repository (no .git folder here).')
125
+
126
+ issues = parse_md_issues(file)
127
+ print(f'[itw] Parsed {len(issues)} task(s) from {file}.')
128
+
129
+ token = ensure_gitlab_token(ctx)
130
+ host, project_path = _parse_gitlab_remote(ctx)
131
+ api_host = _GITLAB_API_HOST_MAP.get(host, host)
132
+
133
+ print(f'[itw] Looking for _pm repos under {project_path!r}...')
134
+ candidates = resolve_pm_repo(api_host, project_path, token)
135
+ if not candidates:
136
+ raise RuntimeError(
137
+ f'No _pm repo found anywhere in the namespace of {project_path!r}.'
138
+ )
139
+
140
+ if len(candidates) == 1:
141
+ chosen = candidates[0]
142
+ print(f'[itw] Using _pm repo: {chosen["path_with_namespace"]}')
143
+ else:
144
+ if not sys.stdin.isatty():
145
+ names = ', '.join(p['path_with_namespace'] for p in candidates)
146
+ raise RuntimeError(
147
+ f'{len(candidates)} _pm repos found ({names}) and stdin is not a TTY — cannot prompt.'
148
+ )
149
+ print(f'[itw] Found {len(candidates)} _pm repos:')
150
+ for i, p in enumerate(candidates, start=1):
151
+ print(f' [{i}] {p["path_with_namespace"]}')
152
+ while True:
153
+ raw = input(f'Pick one (1-{len(candidates)}): ').strip()
154
+ try:
155
+ idx = int(raw)
156
+ if 1 <= idx <= len(candidates):
157
+ chosen = candidates[idx - 1]
158
+ break
159
+ except ValueError:
160
+ pass
161
+ print('Invalid choice, try again.')
162
+
163
+ for idx, (title, body) in enumerate(issues, start=1):
164
+ try:
165
+ result = create_gitlab_task(api_host, chosen['id'], title, body, token)
166
+ except Exception as exc:
167
+ print(f'[itw] Failed at task {idx}/{len(issues)} ({title!r}): {exc}')
168
+ print(f'[itw] {idx - 1} task(s) were created successfully before this failure.')
169
+ raise
170
+ print(f'[itw] Created: {result["web_url"]}')
171
+
172
+
109
173
  def build_frontend(ctx: Context, branch: str, ssr: bool = False) -> None:
110
174
  """Build the Angular app. With ssr=True uses the SSR npm scripts."""
111
175
  if ssr:
@@ -277,7 +341,7 @@ def tag_build_push(ctx: Context, version: Version, skip_pipeline: bool = False,
277
341
  def taginit(ctx: Context) -> None:
278
342
  """Initialize version tagging"""
279
343
  check_branch(ctx)
280
- version = Version(0, 1, 0, 1)
344
+ version = Version(0, 0, 1, 1)
281
345
  tag(ctx, version)
282
346
  save_version(version)
283
347
 
@@ -26,7 +26,8 @@
26
26
  {COMMIT_MESSAGE}
27
27
  </div>
28
28
  <p style="margin:0 0 8px 0;font-size:15px;"><strong>Komanda për të bërë upgrade:</strong></p>
29
- <pre style="background:#0f172a;color:#f9fafb;padding:14px 16px;border-radius:6px;font-size:13px;overflow-x:auto;margin:0 0 20px 0;">pip install itw-python-builder=={VERSION} --no-cache-dir</pre>
29
+ <p style="margin:0 0 6px 0;font-size:12px;color:#6b7280;">Klikoni mbi komandën për ta zgjedhur, pastaj Ctrl+C (ose ⌘+C).</p>
30
+ <pre style="background:#0f172a;color:#f9fafb;padding:14px 16px;border-radius:6px;font-size:13px;overflow-x:auto;margin:0 0 20px 0;-webkit-user-select:all;-moz-user-select:all;-ms-user-select:all;user-select:all;cursor:text;">pip install itw-python-builder=={VERSION} --no-cache-dir</pre>
30
31
  <p style="margin:0;font-size:13px;color:#6b7280;">Faleminderit</p>
31
32
  </td>
32
33
  </tr>
@@ -420,3 +420,138 @@ def is_ssr_project() -> bool:
420
420
  os.path.isfile(os.path.join(cwd, 'server.ts'))
421
421
  and os.path.isfile(os.path.join(cwd, 'tsconfig.server.json'))
422
422
  )
423
+
424
+
425
+ def _gitlab_api_call(api_host: str, endpoint: str, token: str, method: str = 'GET', payload=None):
426
+ """Make a JSON call to the GitLab v4 API and return the parsed body."""
427
+ import ssl
428
+ import urllib.error
429
+ import urllib.request
430
+
431
+ url = f'https://{api_host}/api/v4{endpoint}'
432
+ headers = {
433
+ 'PRIVATE-TOKEN': token,
434
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
435
+ }
436
+ data = None
437
+ if payload is not None:
438
+ data = json.dumps(payload).encode('utf-8')
439
+ headers['Content-Type'] = 'application/json'
440
+
441
+ req = urllib.request.Request(url, headers=headers, method=method, data=data)
442
+ ssl_ctx = ssl.create_default_context()
443
+ ssl_ctx.check_hostname = False
444
+ ssl_ctx.verify_mode = ssl.CERT_NONE
445
+ try:
446
+ with urllib.request.urlopen(req, context=ssl_ctx, timeout=15) as resp:
447
+ return json.loads(resp.read().decode('utf-8'))
448
+ except urllib.error.HTTPError as exc:
449
+ body = exc.read().decode('utf-8', errors='replace')[:500]
450
+ raise RuntimeError(f'GitLab API {method} {endpoint} failed ({exc.code}): {body}') from exc
451
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
452
+ raise RuntimeError(f'GitLab API {method} {endpoint} unreachable: {exc}') from exc
453
+
454
+
455
+ def ensure_gitlab_token(ctx: Context) -> str:
456
+ """Resolve a usable GitLab token."""
457
+ import getpass
458
+
459
+ if os.environ.get('GITLAB_TOKEN'):
460
+ return os.environ['GITLAB_TOKEN']
461
+
462
+ host, _ = _parse_gitlab_remote(ctx)
463
+ api_host = {'git.it-works.io': 'gitlab.it-works.io'}.get(host, host)
464
+
465
+ cached = load_cached_token()
466
+ if cached:
467
+ try:
468
+ _gitlab_api_call(api_host, '/user', cached)
469
+ os.environ['GITLAB_TOKEN'] = cached
470
+ print('[itw] Using cached GitLab token.')
471
+ return cached
472
+ except RuntimeError as exc:
473
+ msg = str(exc)
474
+ if '401' in msg or '403' in msg:
475
+ print('[itw] Cached GitLab token rejected. Please re-enter.')
476
+ else:
477
+ print(f'[itw] Could not verify cached token ({exc}); using it anyway.')
478
+ os.environ['GITLAB_TOKEN'] = cached
479
+ return cached
480
+
481
+ token = getpass.getpass('Enter GitLab token: ')
482
+ if not token:
483
+ raise RuntimeError('No token entered; aborting.')
484
+ os.environ['GITLAB_TOKEN'] = token
485
+ save_token_to_cache(token)
486
+ return token
487
+
488
+
489
+ def _list_group_projects(api_host: str, group_path: str, token: str) -> list:
490
+ """List up to 100 projects in a group matching '_pm'."""
491
+ from urllib.parse import quote
492
+ encoded = quote(group_path, safe='')
493
+ return _gitlab_api_call(
494
+ api_host,
495
+ f'/groups/{encoded}/projects?per_page=100&search=_pm',
496
+ token,
497
+ )
498
+
499
+
500
+ def resolve_pm_repo(api_host: str, project_path: str, token: str) -> list:
501
+ """Find _pm repos by walking the namespace upward from project_path."""
502
+ parts = project_path.split('/')
503
+ if len(parts) < 2:
504
+ raise RuntimeError(f'Project path {project_path!r} has no parent group.')
505
+ parts.pop()
506
+ while parts:
507
+ namespace = '/'.join(parts)
508
+ try:
509
+ projects = _list_group_projects(api_host, namespace, token)
510
+ except RuntimeError as exc:
511
+ if '404' in str(exc):
512
+ parts.pop()
513
+ continue
514
+ raise
515
+ matches = [p for p in projects if p.get('path', '').endswith('_pm')]
516
+ if matches:
517
+ return matches
518
+ parts.pop()
519
+ return []
520
+
521
+
522
+ def create_gitlab_task(api_host: str, project_id: int, title: str, description: str, token: str) -> dict:
523
+ """Create a GitLab task in the given project."""
524
+ return _gitlab_api_call(
525
+ api_host,
526
+ f'/projects/{project_id}/issues',
527
+ token,
528
+ method='POST',
529
+ payload={'title': title, 'description': description, 'issue_type': 'task'},
530
+ )
531
+
532
+
533
+ def parse_md_issues(file_path: str) -> list:
534
+ """Parse a markdown file into (title, body) blocks separated by '==='."""
535
+ with open(file_path, 'r', encoding='utf-8') as fp:
536
+ content = fp.read().replace('\r\n', '\n')
537
+ blocks = re.split(r'(?m)^\s*===\s*$', content)
538
+ issues = []
539
+ for idx, block in enumerate(blocks, start=1):
540
+ block = block.strip()
541
+ if not block:
542
+ continue
543
+ lines = block.splitlines()
544
+ title = None
545
+ body_start = 0
546
+ for i, line in enumerate(lines):
547
+ if line.strip():
548
+ title = line.lstrip('#').strip()
549
+ body_start = i + 1
550
+ break
551
+ if not title:
552
+ raise RuntimeError(f'Block {idx} in {file_path}: missing title (first non-empty line).')
553
+ body = '\n'.join(lines[body_start:]).strip()
554
+ issues.append((title, body))
555
+ if not issues:
556
+ raise RuntimeError(f'No issues found in {file_path}.')
557
+ return issues
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itw_python_builder
3
- Version: 0.2.0
3
+ Version: 0.2.2
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.2.0"
7
+ version = "0.2.2"
8
8
  description = "Standardized Django deployment pipeline with Docker, testing, and SonarQube integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"