django-cfg 1.4.72__py3-none-any.whl → 1.4.74__py3-none-any.whl

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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (31) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/endpoints/README.md +144 -0
  3. django_cfg/apps/api/endpoints/endpoints_status/__init__.py +13 -0
  4. django_cfg/apps/api/endpoints/urls.py +13 -6
  5. django_cfg/apps/api/endpoints/urls_list/__init__.py +10 -0
  6. django_cfg/apps/api/endpoints/urls_list/serializers.py +74 -0
  7. django_cfg/apps/api/endpoints/urls_list/views.py +231 -0
  8. django_cfg/apps/api/health/drf_views.py +9 -0
  9. django_cfg/apps/api/health/serializers.py +4 -0
  10. django_cfg/models/django/crypto_fields.py +11 -0
  11. django_cfg/modules/django_client/core/__init__.py +2 -1
  12. django_cfg/modules/django_client/core/archive/manager.py +14 -0
  13. django_cfg/modules/django_client/core/generator/__init__.py +40 -2
  14. django_cfg/modules/django_client/core/generator/proto/__init__.py +17 -0
  15. django_cfg/modules/django_client/core/generator/proto/generator.py +461 -0
  16. django_cfg/modules/django_client/core/generator/proto/messages_generator.py +260 -0
  17. django_cfg/modules/django_client/core/generator/proto/services_generator.py +295 -0
  18. django_cfg/modules/django_client/core/generator/proto/test_proto_generator.py +262 -0
  19. django_cfg/modules/django_client/core/generator/proto/type_mapper.py +153 -0
  20. django_cfg/modules/django_client/management/commands/generate_client.py +49 -3
  21. django_cfg/pyproject.toml +1 -1
  22. {django_cfg-1.4.72.dist-info → django_cfg-1.4.74.dist-info}/METADATA +1 -1
  23. {django_cfg-1.4.72.dist-info → django_cfg-1.4.74.dist-info}/RECORD +31 -20
  24. /django_cfg/apps/api/endpoints/{checker.py → endpoints_status/checker.py} +0 -0
  25. /django_cfg/apps/api/endpoints/{drf_views.py → endpoints_status/drf_views.py} +0 -0
  26. /django_cfg/apps/api/endpoints/{serializers.py → endpoints_status/serializers.py} +0 -0
  27. /django_cfg/apps/api/endpoints/{tests.py → endpoints_status/tests.py} +0 -0
  28. /django_cfg/apps/api/endpoints/{views.py → endpoints_status/views.py} +0 -0
  29. {django_cfg-1.4.72.dist-info → django_cfg-1.4.74.dist-info}/WHEEL +0 -0
  30. {django_cfg-1.4.72.dist-info → django_cfg-1.4.74.dist-info}/entry_points.txt +0 -0
  31. {django_cfg-1.4.72.dist-info → django_cfg-1.4.74.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.72"
35
+ __version__ = "1.4.74"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -0,0 +1,144 @@
1
+ # Django CFG Endpoints API
2
+
3
+ Модуль для работы с Django URL endpoints.
4
+
5
+ ## Структура
6
+
7
+ ```
8
+ endpoints/
9
+ ├── endpoints_status/ # Проверка статуса всех endpoints
10
+ │ ├── checker.py # Логика проверки endpoints
11
+ │ ├── drf_views.py # DRF views
12
+ │ ├── serializers.py # Serializers
13
+ │ ├── tests.py # Tests
14
+ │ └── views.py # Plain Django views
15
+ ├── urls_list/ # Список всех URL в Django
16
+ │ ├── views.py # DRF views для вывода URLs
17
+ │ └── serializers.py # Serializers
18
+ └── urls.py # URL routing
19
+ ```
20
+
21
+ ## Endpoints
22
+
23
+ ### Endpoints Status
24
+
25
+ Проверяет здоровье всех зарегистрированных endpoints в Django.
26
+
27
+ - **DRF (Browsable)**: `/cfg/endpoints/drf/`
28
+ - **JSON**: `/cfg/endpoints/`
29
+
30
+ **Query параметры:**
31
+ - `include_unnamed` (bool): Включить endpoints без имени (default: false)
32
+ - `timeout` (int): Timeout запроса в секундах (default: 5)
33
+ - `auto_auth` (bool): Автоматически повторить с JWT при 401/403 (default: true)
34
+
35
+ ### URLs List
36
+
37
+ Выводит список всех зарегистрированных URL patterns в Django.
38
+
39
+ - **Full details**: `/cfg/endpoints/urls/`
40
+ - **Compact**: `/cfg/endpoints/urls/compact/`
41
+
42
+ **Compact** возвращает только pattern + name для каждого URL.
43
+
44
+ **Full details** возвращает:
45
+ - Pattern (regex или typed)
46
+ - Name
47
+ - Full name (с namespace)
48
+ - Namespace
49
+ - View name
50
+ - View class
51
+ - HTTP methods
52
+ - Module path
53
+
54
+ ## Примеры
55
+
56
+ ### URLs List (Full)
57
+
58
+ ```bash
59
+ curl http://localhost:8000/cfg/endpoints/urls/
60
+ ```
61
+
62
+ ```json
63
+ {
64
+ "status": "success",
65
+ "service": "Django CFG",
66
+ "version": "2.0.0",
67
+ "base_url": "http://localhost:8000",
68
+ "total_urls": 150,
69
+ "urls": [
70
+ {
71
+ "pattern": "/api/accounts/profile/",
72
+ "name": "account_profile",
73
+ "full_name": "api:account_profile",
74
+ "namespace": "api",
75
+ "view": "ProfileViewSet",
76
+ "view_class": "ProfileViewSet",
77
+ "methods": ["get", "post", "put", "patch", "delete"],
78
+ "module": "apps.accounts.views"
79
+ },
80
+ ...
81
+ ]
82
+ }
83
+ ```
84
+
85
+ ### URLs List (Compact)
86
+
87
+ ```bash
88
+ curl http://localhost:8000/cfg/endpoints/urls/compact/
89
+ ```
90
+
91
+ ```json
92
+ {
93
+ "status": "success",
94
+ "total": 150,
95
+ "urls": [
96
+ {
97
+ "pattern": "/api/accounts/profile/",
98
+ "name": "account_profile"
99
+ },
100
+ ...
101
+ ]
102
+ }
103
+ ```
104
+
105
+ ### Endpoints Status
106
+
107
+ ```bash
108
+ curl http://localhost:8000/cfg/endpoints/drf/
109
+ ```
110
+
111
+ ```json
112
+ {
113
+ "status": "healthy",
114
+ "timestamp": "2025-10-26T10:30:00Z",
115
+ "total_endpoints": 100,
116
+ "healthy": 95,
117
+ "unhealthy": 0,
118
+ "warnings": 3,
119
+ "errors": 0,
120
+ "skipped": 2,
121
+ "endpoints": [...]
122
+ }
123
+ ```
124
+
125
+ ## Health Check Integration
126
+
127
+ URLs list endpoint доступен из health check:
128
+
129
+ ```bash
130
+ curl http://localhost:8000/cfg/health/drf/
131
+ ```
132
+
133
+ ```json
134
+ {
135
+ "status": "healthy",
136
+ ...
137
+ "links": {
138
+ "urls_list": "http://localhost:8000/cfg/endpoints/urls/",
139
+ "urls_list_compact": "http://localhost:8000/cfg/endpoints/urls/compact/",
140
+ "endpoints_status": "http://localhost:8000/cfg/endpoints/drf/",
141
+ "quick_health": "http://localhost:8000/cfg/health/drf/quick/"
142
+ }
143
+ }
144
+ ```
@@ -0,0 +1,13 @@
1
+ """
2
+ Django CFG Endpoints Status module.
3
+ """
4
+
5
+ from .checker import check_all_endpoints
6
+ from .drf_views import DRFEndpointsStatusView
7
+ from .views import EndpointsStatusView
8
+
9
+ __all__ = [
10
+ 'check_all_endpoints',
11
+ 'DRFEndpointsStatusView',
12
+ 'EndpointsStatusView',
13
+ ]
@@ -1,15 +1,22 @@
1
1
  """
2
- Django CFG Endpoints Status URLs.
2
+ Django CFG Endpoints URLs.
3
3
  """
4
4
 
5
5
  from django.urls import path
6
6
 
7
- from . import drf_views, views
7
+ from .endpoints_status import DRFEndpointsStatusView, EndpointsStatusView
8
+ from .urls_list import DRFURLsListCompactView, DRFURLsListView
8
9
 
9
10
  urlpatterns = [
10
- # Original JSON endpoint
11
- path('', views.EndpointsStatusView.as_view(), name='endpoints_status'),
11
+ # Endpoints Status - Original JSON endpoint
12
+ path('', EndpointsStatusView.as_view(), name='endpoints_status'),
12
13
 
13
- # DRF Browsable API endpoint with Tailwind theme
14
- path('drf/', drf_views.DRFEndpointsStatusView.as_view(), name='endpoints_status_drf'),
14
+ # Endpoints Status - DRF Browsable API endpoint with Tailwind theme
15
+ path('drf/', DRFEndpointsStatusView.as_view(), name='endpoints_status_drf'),
16
+
17
+ # URLs List - Full details
18
+ path('urls/', DRFURLsListView.as_view(), name='urls_list'),
19
+
20
+ # URLs List - Compact (pattern + name only)
21
+ path('urls/compact/', DRFURLsListCompactView.as_view(), name='urls_list_compact'),
15
22
  ]
@@ -0,0 +1,10 @@
1
+ """
2
+ Django CFG URLs List module.
3
+ """
4
+
5
+ from .views import DRFURLsListCompactView, DRFURLsListView
6
+
7
+ __all__ = [
8
+ 'DRFURLsListView',
9
+ 'DRFURLsListCompactView',
10
+ ]
@@ -0,0 +1,74 @@
1
+ """
2
+ Django CFG URLs List Serializers
3
+
4
+ DRF serializers for URLs list API.
5
+ """
6
+
7
+ from rest_framework import serializers
8
+
9
+
10
+ class URLPatternSerializer(serializers.Serializer):
11
+ """Serializer for single URL pattern."""
12
+
13
+ pattern = serializers.CharField(
14
+ help_text="URL pattern (e.g., ^api/users/(?P<pk>[^/.]+)/$)"
15
+ )
16
+ name = serializers.CharField(
17
+ required=False,
18
+ allow_null=True,
19
+ help_text="URL name (if defined)"
20
+ )
21
+ full_name = serializers.CharField(
22
+ required=False,
23
+ allow_null=True,
24
+ help_text="Full URL name with namespace (e.g., admin:index)"
25
+ )
26
+ namespace = serializers.CharField(
27
+ required=False,
28
+ allow_null=True,
29
+ help_text="URL namespace"
30
+ )
31
+ view = serializers.CharField(
32
+ required=False,
33
+ allow_null=True,
34
+ help_text="View function/class name"
35
+ )
36
+ view_class = serializers.CharField(
37
+ required=False,
38
+ allow_null=True,
39
+ help_text="View class name (for CBV/ViewSets)"
40
+ )
41
+ methods = serializers.ListField(
42
+ child=serializers.CharField(),
43
+ required=False,
44
+ help_text="Allowed HTTP methods"
45
+ )
46
+ module = serializers.CharField(
47
+ required=False,
48
+ allow_null=True,
49
+ help_text="View module path"
50
+ )
51
+
52
+
53
+ class URLsListSerializer(serializers.Serializer):
54
+ """Serializer for URLs list response."""
55
+
56
+ status = serializers.CharField(
57
+ help_text="Status: success or error"
58
+ )
59
+ service = serializers.CharField(
60
+ help_text="Service name"
61
+ )
62
+ version = serializers.CharField(
63
+ help_text="Django-CFG version"
64
+ )
65
+ base_url = serializers.CharField(
66
+ help_text="Base URL of the service"
67
+ )
68
+ total_urls = serializers.IntegerField(
69
+ help_text="Total number of registered URLs"
70
+ )
71
+ urls = URLPatternSerializer(
72
+ many=True,
73
+ help_text="List of all registered URL patterns"
74
+ )
@@ -0,0 +1,231 @@
1
+ """
2
+ Django CFG URLs List DRF View
3
+
4
+ DRF browsable API view for listing all Django URLs with Tailwind theme support.
5
+ """
6
+
7
+ from typing import Any, Dict, List
8
+ from urllib.parse import urljoin
9
+
10
+ from django.conf import settings
11
+ from django.urls import URLPattern, URLResolver, get_resolver
12
+ from rest_framework import status
13
+ from rest_framework.permissions import AllowAny
14
+ from rest_framework.response import Response
15
+ from rest_framework.views import APIView
16
+
17
+ from django_cfg.core.integration import get_current_version
18
+
19
+ from .serializers import URLsListSerializer
20
+
21
+
22
+ class DRFURLsListView(APIView):
23
+ """
24
+ Django CFG URLs list endpoint with DRF Browsable API.
25
+
26
+ Lists all registered Django URLs with their:
27
+ - Pattern
28
+ - Name
29
+ - View/ViewSet
30
+ - Full URL
31
+ - HTTP methods
32
+
33
+ This endpoint uses DRF Browsable API with Tailwind CSS theme! 🎨
34
+ """
35
+
36
+ permission_classes = [AllowAny] # Public endpoint (can be restricted)
37
+ serializer_class = URLsListSerializer # For schema generation
38
+
39
+ def get(self, request):
40
+ """Return all registered URLs."""
41
+ try:
42
+ config = getattr(settings, 'config', None)
43
+
44
+ # Get base URL from config or settings
45
+ base_url = getattr(config, 'site_url', None) if config else None
46
+ if not base_url:
47
+ base_url = request.build_absolute_uri('/').rstrip('/')
48
+
49
+ urls_data = {
50
+ "status": "success",
51
+ "service": config.project_name if config else "Django CFG",
52
+ "version": get_current_version(),
53
+ "base_url": base_url,
54
+ "total_urls": 0,
55
+ "urls": []
56
+ }
57
+
58
+ # Extract all URLs
59
+ url_patterns = self._get_all_urls()
60
+ urls_data["urls"] = url_patterns
61
+ urls_data["total_urls"] = len(url_patterns)
62
+
63
+ return Response(urls_data, status=status.HTTP_200_OK)
64
+
65
+ except Exception as e:
66
+ return Response({
67
+ "status": "error",
68
+ "error": str(e)
69
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
70
+
71
+ def _get_all_urls(self, urlpatterns=None, prefix='', namespace=None) -> List[Dict[str, Any]]:
72
+ """
73
+ Recursively extract all URL patterns from Django URLconf.
74
+
75
+ Args:
76
+ urlpatterns: URL patterns to process
77
+ prefix: URL prefix from parent resolvers
78
+ namespace: Current namespace
79
+
80
+ Returns:
81
+ List of URL pattern dictionaries
82
+ """
83
+ if urlpatterns is None:
84
+ urlpatterns = get_resolver().url_patterns
85
+
86
+ url_list = []
87
+
88
+ for pattern in urlpatterns:
89
+ if isinstance(pattern, URLResolver):
90
+ # Recursively process URL resolver (include())
91
+ new_prefix = prefix + str(pattern.pattern)
92
+ new_namespace = f"{namespace}:{pattern.namespace}" if namespace and pattern.namespace else pattern.namespace or namespace
93
+
94
+ url_list.extend(
95
+ self._get_all_urls(
96
+ pattern.url_patterns,
97
+ prefix=new_prefix,
98
+ namespace=new_namespace
99
+ )
100
+ )
101
+ elif isinstance(pattern, URLPattern):
102
+ # Extract URL pattern details
103
+ url_pattern = prefix + str(pattern.pattern)
104
+ url_name = pattern.name
105
+
106
+ # Build full name with namespace
107
+ if namespace and url_name:
108
+ full_name = f"{namespace}:{url_name}"
109
+ else:
110
+ full_name = url_name
111
+
112
+ # Get view information
113
+ view_info = self._get_view_info(pattern)
114
+
115
+ url_list.append({
116
+ "pattern": url_pattern,
117
+ "name": url_name,
118
+ "full_name": full_name,
119
+ "namespace": namespace,
120
+ "view": view_info["view"],
121
+ "view_class": view_info["view_class"],
122
+ "methods": view_info["methods"],
123
+ "module": view_info["module"],
124
+ })
125
+
126
+ return url_list
127
+
128
+ def _get_view_info(self, pattern: URLPattern) -> Dict[str, Any]:
129
+ """
130
+ Extract view information from URL pattern.
131
+
132
+ Args:
133
+ pattern: URLPattern instance
134
+
135
+ Returns:
136
+ Dictionary with view information
137
+ """
138
+ view_info = {
139
+ "view": None,
140
+ "view_class": None,
141
+ "methods": [],
142
+ "module": None,
143
+ }
144
+
145
+ try:
146
+ callback = pattern.callback
147
+
148
+ if callback is None:
149
+ return view_info
150
+
151
+ # Get view name
152
+ if hasattr(callback, '__name__'):
153
+ view_info["view"] = callback.__name__
154
+ elif hasattr(callback, '__class__'):
155
+ view_info["view"] = callback.__class__.__name__
156
+
157
+ # Get view class (for CBV/ViewSets)
158
+ if hasattr(callback, 'cls'):
159
+ view_info["view_class"] = callback.cls.__name__
160
+
161
+ # Get HTTP methods from ViewSet/APIView
162
+ if hasattr(callback.cls, 'http_method_names'):
163
+ view_info["methods"] = callback.cls.http_method_names
164
+
165
+ # Get module
166
+ if hasattr(callback.cls, '__module__'):
167
+ view_info["module"] = callback.cls.__module__
168
+
169
+ # For function-based views
170
+ elif hasattr(callback, '__module__'):
171
+ view_info["module"] = callback.__module__
172
+
173
+ # Try to determine methods from decorator
174
+ if hasattr(callback, 'methods'):
175
+ view_info["methods"] = list(callback.methods)
176
+ else:
177
+ view_info["methods"] = ['GET'] # Default for FBV
178
+
179
+ except Exception:
180
+ pass
181
+
182
+ return view_info
183
+
184
+
185
+ class DRFURLsListCompactView(APIView):
186
+ """
187
+ Compact URLs list endpoint - just patterns and names.
188
+
189
+ This endpoint uses DRF Browsable API with Tailwind CSS theme! 🎨
190
+ """
191
+
192
+ permission_classes = [AllowAny]
193
+
194
+ def get(self, request):
195
+ """Return compact URL list."""
196
+ try:
197
+ url_patterns = self._get_compact_urls()
198
+
199
+ return Response({
200
+ "status": "success",
201
+ "total": len(url_patterns),
202
+ "urls": url_patterns
203
+ }, status=status.HTTP_200_OK)
204
+
205
+ except Exception as e:
206
+ return Response({
207
+ "status": "error",
208
+ "error": str(e)
209
+ }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
210
+
211
+ def _get_compact_urls(self, urlpatterns=None, prefix='') -> List[Dict[str, str]]:
212
+ """Extract URLs in compact format."""
213
+ if urlpatterns is None:
214
+ urlpatterns = get_resolver().url_patterns
215
+
216
+ url_list = []
217
+
218
+ for pattern in urlpatterns:
219
+ if isinstance(pattern, URLResolver):
220
+ new_prefix = prefix + str(pattern.pattern)
221
+ url_list.extend(
222
+ self._get_compact_urls(pattern.url_patterns, prefix=new_prefix)
223
+ )
224
+ elif isinstance(pattern, URLPattern):
225
+ url_pattern = prefix + str(pattern.pattern)
226
+ url_list.append({
227
+ "pattern": url_pattern,
228
+ "name": pattern.name or "unnamed",
229
+ })
230
+
231
+ return url_list
@@ -13,6 +13,7 @@ import psutil
13
13
  from django.conf import settings
14
14
  from django.core.cache import cache
15
15
  from django.db import connections
16
+ from django.urls import reverse
16
17
  from django.utils import timezone
17
18
  from rest_framework import status
18
19
  from rest_framework.permissions import AllowAny
@@ -102,6 +103,14 @@ class DRFHealthCheckView(APIView):
102
103
  "python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}.{os.sys.version_info.micro}",
103
104
  }
104
105
 
106
+ # Add useful links using reverse()
107
+ health_data["links"] = {
108
+ "urls_list": request.build_absolute_uri(reverse('urls_list')),
109
+ "urls_list_compact": request.build_absolute_uri(reverse('urls_list_compact')),
110
+ "endpoints_status": request.build_absolute_uri(reverse('endpoints_status_drf')),
111
+ "quick_health": request.build_absolute_uri(reverse('django_cfg_drf_quick_health')),
112
+ }
113
+
105
114
  # Return appropriate HTTP status
106
115
  http_status = status.HTTP_200_OK
107
116
  if health_data["status"] == "unhealthy":
@@ -28,6 +28,10 @@ class HealthCheckSerializer(serializers.Serializer):
28
28
  environment = serializers.DictField(
29
29
  help_text="Environment information"
30
30
  )
31
+ links = serializers.DictField(
32
+ required=False,
33
+ help_text="Useful API endpoint links"
34
+ )
31
35
 
32
36
 
33
37
  class QuickHealthSerializer(serializers.Serializer):
@@ -58,6 +58,12 @@ class CryptoFieldsConfig(BaseModel):
58
58
  description="Auto-create encryption keys if missing (None = auto: True in DEBUG, False in production)"
59
59
  )
60
60
 
61
+ # === Django Revision Settings (for django-audit-fields dependency) ===
62
+ ignore_git_dir: bool = Field(
63
+ default=True,
64
+ description="Ignore git directory for django-revision (set DJANGO_REVISION_IGNORE_WORKING_DIR=True)"
65
+ )
66
+
61
67
  def to_django_settings(self, base_dir: Path, is_production: bool, debug: bool) -> dict:
62
68
  """
63
69
  Convert to Django settings dictionary with environment-aware defaults.
@@ -91,6 +97,11 @@ class CryptoFieldsConfig(BaseModel):
91
97
  "AUTO_CREATE_KEYS": auto_create_keys,
92
98
  }
93
99
 
100
+ # Disable django-revision git integration (required by django-audit-fields)
101
+ # RevisionField will use package metadata or pyproject.toml instead
102
+ if self.ignore_git_dir:
103
+ settings["DJANGO_REVISION_IGNORE_WORKING_DIR"] = True
104
+
94
105
  return settings
95
106
 
96
107
  @property
@@ -19,7 +19,7 @@ from .config import (
19
19
  )
20
20
 
21
21
  # Generators
22
- from .generator import GoGenerator, PythonGenerator, TypeScriptGenerator
22
+ from .generator import GoGenerator, ProtoGenerator, PythonGenerator, TypeScriptGenerator
23
23
 
24
24
  # Groups
25
25
  from .groups import GroupDetector, GroupManager
@@ -53,4 +53,5 @@ __all__ = [
53
53
  "PythonGenerator",
54
54
  "TypeScriptGenerator",
55
55
  "GoGenerator",
56
+ "ProtoGenerator",
56
57
  ]
@@ -34,6 +34,8 @@ class ArchiveManager:
34
34
  group_name: str,
35
35
  python_dir: Optional[Path] = None,
36
36
  typescript_dir: Optional[Path] = None,
37
+ go_dir: Optional[Path] = None,
38
+ proto_dir: Optional[Path] = None,
37
39
  ) -> Dict:
38
40
  """
39
41
  Archive generated clients.
@@ -42,6 +44,8 @@ class ArchiveManager:
42
44
  group_name: Name of the group
43
45
  python_dir: Python client directory
44
46
  typescript_dir: TypeScript client directory
47
+ go_dir: Go client directory
48
+ proto_dir: Protocol Buffer definitions directory
45
49
 
46
50
  Returns:
47
51
  Archive result dictionary
@@ -65,6 +69,16 @@ class ArchiveManager:
65
69
  shutil.copytree(typescript_dir, dest, dirs_exist_ok=True)
66
70
  copied["typescript"] = str(dest)
67
71
 
72
+ if go_dir and go_dir.exists():
73
+ dest = archive_path / "go"
74
+ shutil.copytree(go_dir, dest, dirs_exist_ok=True)
75
+ copied["go"] = str(dest)
76
+
77
+ if proto_dir and proto_dir.exists():
78
+ dest = archive_path / "proto"
79
+ shutil.copytree(proto_dir, dest, dirs_exist_ok=True)
80
+ copied["proto"] = str(dest)
81
+
68
82
  # Create metadata
69
83
  metadata = {
70
84
  "group": group_name,