|
1 | 1 | import json |
2 | 2 | from dataclasses import dataclass |
| 3 | +from urllib.error import HTTPError |
3 | 4 | from urllib.request import Request, urlopen |
4 | 5 |
|
5 | 6 | from django.conf import settings |
@@ -386,6 +387,13 @@ class ProductSecurityRoadmapBridgeResult: |
386 | 387 | snapshot: ProductSecuritySnapshotListItem | None |
387 | 388 |
|
388 | 389 |
|
| 390 | +@dataclass(frozen=True) |
| 391 | +class ProductSecurityRoadmapTaskUpdateBridgeResult: |
| 392 | + product_id: int |
| 393 | + roadmap_id: int |
| 394 | + task: ProductSecurityRoadmapTaskItem |
| 395 | + |
| 396 | + |
389 | 397 | class ProductSecurityRustClient: |
390 | 398 | @staticmethod |
391 | 399 | def _base_url() -> str: |
@@ -496,6 +504,49 @@ def fetch_roadmap(request, tenant, product_id: int, timeout: int = 8) -> Product |
496 | 504 | snapshot=ProductSecurityRustClient._snapshot_from_payload(snapshot_payload, tenant) if isinstance(snapshot_payload, dict) else None, |
497 | 505 | ) |
498 | 506 |
|
| 507 | + @staticmethod |
| 508 | + def update_roadmap_task( |
| 509 | + request, |
| 510 | + tenant, |
| 511 | + task_id: int, |
| 512 | + data: dict, |
| 513 | + timeout: int = 8, |
| 514 | + ) -> ProductSecurityRoadmapTaskUpdateBridgeResult | None: |
| 515 | + base = ProductSecurityRustClient._base_url() |
| 516 | + if not base: |
| 517 | + raise RuntimeError('RUST_BACKEND_URL ist nicht gesetzt.') |
| 518 | + |
| 519 | + rust_request = ProductSecurityRustClient._authenticated_json_request( |
| 520 | + request, |
| 521 | + tenant, |
| 522 | + f'{base}/api/v1/product-security/roadmap-tasks/{int(task_id)}', |
| 523 | + { |
| 524 | + 'status': data.get('status'), |
| 525 | + 'priority': data.get('priority') or '', |
| 526 | + 'owner_role': data.get('owner_role') or '', |
| 527 | + 'due_in_days': data.get('due_in_days'), |
| 528 | + 'dependency_text': data.get('dependency_text') or '', |
| 529 | + }, |
| 530 | + method='PATCH', |
| 531 | + ) |
| 532 | + try: |
| 533 | + with urlopen(rust_request, timeout=timeout) as response: |
| 534 | + payload = json.loads(response.read().decode('utf-8')) |
| 535 | + except HTTPError as exc: |
| 536 | + if exc.code == 404: |
| 537 | + return None |
| 538 | + raise |
| 539 | + |
| 540 | + task_payload = payload.get('task') or {} |
| 541 | + if not isinstance(task_payload, dict): |
| 542 | + raise RuntimeError('Rust-Product-Security-Roadmaptask-Update hat kein task-Objekt geliefert.') |
| 543 | + |
| 544 | + return ProductSecurityRoadmapTaskUpdateBridgeResult( |
| 545 | + product_id=ProductSecurityRustClient._int_field(payload, 'product_id'), |
| 546 | + roadmap_id=ProductSecurityRustClient._int_field(payload, 'roadmap_id'), |
| 547 | + task=ProductSecurityRustClient._roadmap_task_from_payload(task_payload, tenant), |
| 548 | + ) |
| 549 | + |
499 | 550 | @staticmethod |
500 | 551 | def _product_from_payload(item: dict, tenant) -> ProductSecurityProductListItem: |
501 | 552 | family_id = ProductSecurityRustClient._optional_int_field(item, 'family_id') |
@@ -801,6 +852,14 @@ def _authenticated_request(request, tenant, url: str) -> Request: |
801 | 852 | rust_request.add_header('X-ISCY-User-Email', user_email) |
802 | 853 | return rust_request |
803 | 854 |
|
| 855 | + @staticmethod |
| 856 | + def _authenticated_json_request(request, tenant, url: str, payload: dict, method: str) -> Request: |
| 857 | + rust_request = ProductSecurityRustClient._authenticated_request(request, tenant, url) |
| 858 | + rust_request.data = json.dumps(payload).encode('utf-8') |
| 859 | + rust_request.method = method |
| 860 | + rust_request.add_header('Content-Type', 'application/json') |
| 861 | + return rust_request |
| 862 | + |
804 | 863 | @staticmethod |
805 | 864 | def _int_field(payload: dict, key: str) -> int: |
806 | 865 | return int(payload.get(key) or 0) |
@@ -884,6 +943,27 @@ def fetch_roadmap(request, tenant, product_id: int): |
884 | 943 | raise RuntimeError('Rust product security backend ist aktiv, aber nicht erreichbar.') from exc |
885 | 944 | return None |
886 | 945 |
|
| 946 | + @staticmethod |
| 947 | + def update_roadmap_task(request, tenant, task_id: int, data: dict): |
| 948 | + if tenant is None: |
| 949 | + return None |
| 950 | + |
| 951 | + backend = str(getattr(settings, 'PRODUCT_SECURITY_BACKEND', 'rust_service') or '').strip().lower() |
| 952 | + if backend != 'rust_service': |
| 953 | + return None |
| 954 | + |
| 955 | + if not ProductSecurityRustClient._base_url(): |
| 956 | + if ProductSecurityBridge._strict_rust_mode(): |
| 957 | + raise RuntimeError('Rust product security backend ist aktiv, aber RUST_BACKEND_URL ist nicht gesetzt.') |
| 958 | + return None |
| 959 | + |
| 960 | + try: |
| 961 | + return ProductSecurityRustClient.update_roadmap_task(request, tenant, task_id, data) |
| 962 | + except Exception as exc: |
| 963 | + if ProductSecurityBridge._strict_rust_mode(): |
| 964 | + raise RuntimeError('Rust product security backend ist aktiv, aber nicht erreichbar.') from exc |
| 965 | + return None |
| 966 | + |
887 | 967 | @staticmethod |
888 | 968 | def _strict_rust_mode() -> bool: |
889 | 969 | return bool(getattr(settings, 'RUST_STRICT_MODE', False)) |
0 commit comments