minchoc 0.0.8__py3-none-any.whl → 0.1.0__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 minchoc might be problematic. Click here for more details.
- minchoc/__init__.py +7 -0
- minchoc/admin.py +3 -0
- minchoc/apps.py +4 -0
- minchoc/constants.py +15 -2
- minchoc/filterlex.py +15 -4
- minchoc/filteryacc.py +113 -41
- minchoc/migrations/0002_alter_company_options_alter_package_unique_together_and_more.py +1 -1
- minchoc/models.py +38 -28
- minchoc/parsetab.py +348 -33
- minchoc/urls.py +5 -3
- minchoc/utils.py +18 -5
- minchoc/views.py +112 -65
- minchoc/wsgi.py +3 -0
- minchoc-0.1.0.dist-info/METADATA +114 -0
- minchoc-0.1.0.dist-info/RECORD +20 -0
- {minchoc-0.0.8.dist-info → minchoc-0.1.0.dist-info}/WHEEL +1 -1
- minchoc-0.1.0.dist-info/licenses/LICENSE.txt +18 -0
- minchoc-0.0.8.dist-info/LICENSE.txt +0 -21
- minchoc-0.0.8.dist-info/METADATA +0 -87
- minchoc-0.0.8.dist-info/RECORD +0 -20
minchoc/views.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"""Views."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from io import BytesIO
|
|
2
6
|
from pathlib import Path
|
|
3
7
|
from tempfile import TemporaryDirectory
|
|
4
|
-
from typing import Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
9
|
import logging
|
|
6
10
|
import re
|
|
7
11
|
import zipfile
|
|
@@ -16,23 +20,29 @@ from django.utils.decorators import method_decorator
|
|
|
16
20
|
from django.views import View
|
|
17
21
|
from django.views.decorators.csrf import csrf_exempt
|
|
18
22
|
from django.views.decorators.http import require_http_methods
|
|
23
|
+
from typing_extensions import override
|
|
19
24
|
|
|
20
25
|
from .constants import FEED_XML_POST, FEED_XML_PRE
|
|
21
|
-
from .filteryacc import parser as filter_parser
|
|
26
|
+
from .filteryacc import FIELD_MAPPING, parser as filter_parser
|
|
22
27
|
from .models import Author, NugetUser, Package, Tag
|
|
23
|
-
from .utils import make_entry
|
|
28
|
+
from .utils import make_entry, tag_text_or
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
31
|
+
from _typeshed import SupportsKeysAndGetItem
|
|
32
|
+
from django.core.files.uploadedfile import UploadedFile
|
|
33
|
+
from django.db.models import Field, ForeignObjectRel
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
NUSPEC_FIELD_AUTHORS =
|
|
27
|
-
NUSPEC_FIELD_DESCRIPTION =
|
|
28
|
-
NUSPEC_FIELD_ID =
|
|
29
|
-
NUSPEC_FIELD_PROJECT_URL =
|
|
30
|
-
NUSPEC_FIELD_REQUIRE_LICENSE_ACCEPTANCE =
|
|
31
|
-
NUSPEC_FIELD_SOURCE_URL =
|
|
32
|
-
NUSPEC_FIELD_SUMMARY =
|
|
33
|
-
NUSPEC_FIELD_TAGS =
|
|
34
|
-
NUSPEC_FIELD_TITLE =
|
|
35
|
-
NUSPEC_FIELD_VERSION =
|
|
35
|
+
NUSPEC_NAMESPACES = {'': 'http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd'}
|
|
36
|
+
NUSPEC_FIELD_AUTHORS = 'authors'
|
|
37
|
+
NUSPEC_FIELD_DESCRIPTION = 'description'
|
|
38
|
+
NUSPEC_FIELD_ID = 'id'
|
|
39
|
+
NUSPEC_FIELD_PROJECT_URL = 'projectUrl'
|
|
40
|
+
NUSPEC_FIELD_REQUIRE_LICENSE_ACCEPTANCE = 'requireLicenseAcceptance'
|
|
41
|
+
NUSPEC_FIELD_SOURCE_URL = 'packageSourceUrl'
|
|
42
|
+
NUSPEC_FIELD_SUMMARY = 'summary'
|
|
43
|
+
NUSPEC_FIELD_TAGS = 'tags'
|
|
44
|
+
NUSPEC_FIELD_TITLE = 'title'
|
|
45
|
+
NUSPEC_FIELD_VERSION = 'version'
|
|
36
46
|
NUSPEC_FIELD_MAPPINGS = {
|
|
37
47
|
NUSPEC_FIELD_AUTHORS: 'authors',
|
|
38
48
|
NUSPEC_FIELD_DESCRIPTION: 'description',
|
|
@@ -43,7 +53,7 @@ NUSPEC_FIELD_MAPPINGS = {
|
|
|
43
53
|
NUSPEC_FIELD_SUMMARY: 'summary',
|
|
44
54
|
NUSPEC_FIELD_TAGS: 'tags',
|
|
45
55
|
NUSPEC_FIELD_TITLE: 'title',
|
|
46
|
-
NUSPEC_FIELD_VERSION: 'version'
|
|
56
|
+
NUSPEC_FIELD_VERSION: 'version',
|
|
47
57
|
}
|
|
48
58
|
PACKAGE_FIELDS = {f.name: f for f in Package._meta.get_fields()}
|
|
49
59
|
|
|
@@ -52,14 +62,14 @@ logger = logging.getLogger(__name__)
|
|
|
52
62
|
|
|
53
63
|
@require_http_methods(['GET'])
|
|
54
64
|
def home(_request: HttpRequest) -> HttpResponse:
|
|
55
|
-
"""
|
|
65
|
+
"""Get the content for the static homepage."""
|
|
56
66
|
return JsonResponse({})
|
|
57
67
|
|
|
58
68
|
|
|
59
69
|
@require_http_methods(['GET'])
|
|
60
70
|
def metadata(_request: HttpRequest) -> HttpResponse:
|
|
61
|
-
"""
|
|
62
|
-
return HttpResponse(
|
|
71
|
+
"""Get content for static page at ``/$metadata`` and at ``/api/v2/$metadata``."""
|
|
72
|
+
return HttpResponse("""<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
|
63
73
|
<service xml:base="http://fixme/api/v2/"
|
|
64
74
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
65
75
|
xmlns:app="http://www.w3.org/2007/app"
|
|
@@ -68,28 +78,57 @@ def metadata(_request: HttpRequest) -> HttpResponse:
|
|
|
68
78
|
<atom:title>Default</atom:title>
|
|
69
79
|
<collection href="Packages"><atom:title>Packages</atom:title></collection>
|
|
70
80
|
</workspace>
|
|
71
|
-
</service>\n
|
|
81
|
+
</service>\n""",
|
|
72
82
|
content_type='application/xml')
|
|
73
83
|
|
|
74
84
|
|
|
75
85
|
@require_http_methods(['GET'])
|
|
76
86
|
def find_packages_by_id(request: HttpRequest) -> HttpResponse:
|
|
77
87
|
"""
|
|
78
|
-
|
|
88
|
+
Take a ``GET`` request to find packages.
|
|
79
89
|
|
|
80
90
|
Sample URL: ``/FindPackagesById()?id=package-name``
|
|
91
|
+
|
|
92
|
+
Supports ``$skiptoken`` parameter for pagination in the format:
|
|
93
|
+
``$skiptoken='PackageName','Version'``.
|
|
81
94
|
"""
|
|
82
|
-
if
|
|
95
|
+
if sem_ver_level := request.GET.get('semVerLevel'):
|
|
83
96
|
logger.warning('Ignoring semVerLevel=%s', sem_ver_level)
|
|
84
97
|
proto = 'https' if request.is_secure() else 'http'
|
|
85
98
|
proto_host = f'{proto}://{request.get_host()}'
|
|
86
99
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
nuget_id = request.GET['id'].replace("'", '')
|
|
101
|
+
queryset = Package._default_manager.filter(nuget_id=nuget_id)
|
|
102
|
+
if skiptoken := request.GET.get('$skiptoken'):
|
|
103
|
+
# Parse skiptoken format: `'PackageName','Version'``.
|
|
104
|
+
# Remove quotes and split by comma.
|
|
105
|
+
parts = [part.strip().strip('\'"') for part in skiptoken.split(',')]
|
|
106
|
+
expected_parts = 2
|
|
107
|
+
if len(parts) == expected_parts:
|
|
108
|
+
skip_id, skip_version = parts
|
|
109
|
+
# Filter to get packages after the specified version.
|
|
110
|
+
# We order by version and filter out versions up to and including skip_version.
|
|
111
|
+
queryset = queryset.order_by('version')
|
|
112
|
+
# Get all packages and filter those after the skip_version.
|
|
113
|
+
all_packages: list[Package] = list(queryset)
|
|
114
|
+
skip_index = -1
|
|
115
|
+
for i, pkg in enumerate(all_packages):
|
|
116
|
+
if pkg.nuget_id == skip_id and pkg.version == skip_version:
|
|
117
|
+
skip_index = i
|
|
118
|
+
break
|
|
119
|
+
content = '\n'.join(
|
|
120
|
+
make_entry(proto_host, x)
|
|
121
|
+
for x in (all_packages[skip_index + 1:] if skip_index >= 0 else all_packages))
|
|
122
|
+
return HttpResponse(f'{FEED_XML_PRE}{content}{FEED_XML_POST}\n' % {
|
|
123
|
+
'BASEURL': proto_host,
|
|
124
|
+
'UPDATED': datetime.now(timezone.utc).isoformat()
|
|
125
|
+
},
|
|
126
|
+
content_type='application/xml')
|
|
127
|
+
logger.warning('Invalid $skiptoken format: %s', skiptoken) # pragma: no cover
|
|
128
|
+
content = '\n'.join(make_entry(proto_host, x) for x in queryset)
|
|
90
129
|
return HttpResponse(f'{FEED_XML_PRE}{content}{FEED_XML_POST}\n' % {
|
|
91
130
|
'BASEURL': proto_host,
|
|
92
|
-
'UPDATED': datetime.now().isoformat()
|
|
131
|
+
'UPDATED': datetime.now(timezone.utc).isoformat()
|
|
93
132
|
},
|
|
94
133
|
content_type='application/xml')
|
|
95
134
|
except KeyError:
|
|
@@ -99,31 +138,35 @@ def find_packages_by_id(request: HttpRequest) -> HttpResponse:
|
|
|
99
138
|
@require_http_methods(['GET'])
|
|
100
139
|
def packages(request: HttpRequest) -> HttpResponse:
|
|
101
140
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
141
|
+
Take a ``GET`` request to find packages.
|
|
142
|
+
|
|
143
|
+
Query parameters ``$skip``, ``$top`` and ``semVerLevel`` are ignored. This means pagination is
|
|
144
|
+
currently not supported.
|
|
104
145
|
|
|
105
146
|
Sample URL: ``/Packages()?$orderby=id&$filter=(tolower(Id) eq 'package-name') and IsLatestVersion&$skip=0&$top=1``
|
|
106
147
|
""" # noqa: E501
|
|
107
148
|
filter_ = request.GET.get('$filter')
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
req_order_by = request.GET.get('$orderby')
|
|
150
|
+
order_by = (FIELD_MAPPING[req_order_by]
|
|
151
|
+
if req_order_by and req_order_by in FIELD_MAPPING else 'nuget_id')
|
|
152
|
+
if sem_ver_level := request.GET.get('semVerLevel'):
|
|
110
153
|
logger.warning('Ignoring semVerLevel=%s', sem_ver_level)
|
|
111
|
-
if
|
|
154
|
+
if skip := request.GET.get('$skip'):
|
|
112
155
|
logger.warning('Ignoring $skip=%s', skip)
|
|
113
|
-
if
|
|
156
|
+
if top := request.GET.get('$top'):
|
|
114
157
|
logger.warning('Ignoring $top=%s', top)
|
|
115
158
|
try:
|
|
116
159
|
filters = filter_parser.parse(filter_) if filter_ else {}
|
|
117
160
|
except SyntaxError:
|
|
118
|
-
return JsonResponse({'error': 'Invalid syntax in filter'}, status=400)
|
|
161
|
+
return JsonResponse({'error': 'Invalid syntax in filter.'}, status=400)
|
|
119
162
|
proto = 'https' if request.is_secure() else 'http'
|
|
120
163
|
proto_host = f'{proto}://{request.get_host()}'
|
|
121
164
|
content = '\n'.join(
|
|
122
165
|
make_entry(proto_host, x)
|
|
123
|
-
for x in Package.
|
|
166
|
+
for x in Package._default_manager.order_by(order_by).filter(filters)[0:20])
|
|
124
167
|
return HttpResponse(f'{FEED_XML_PRE}\n{content}{FEED_XML_POST}\n' % {
|
|
125
168
|
'BASEURL': proto_host,
|
|
126
|
-
'UPDATED': datetime.now().isoformat()
|
|
169
|
+
'UPDATED': datetime.now(timezone.utc).isoformat()
|
|
127
170
|
},
|
|
128
171
|
content_type='application/xml')
|
|
129
172
|
|
|
@@ -135,13 +178,13 @@ def packages_with_args(request: HttpRequest, name: str, version: str) -> HttpRes
|
|
|
135
178
|
|
|
136
179
|
Sample URL: ``/Packages(Id='name',Version='123.0.0')``
|
|
137
180
|
"""
|
|
138
|
-
if
|
|
181
|
+
if package := Package._default_manager.filter(nuget_id=name, version=version).first():
|
|
139
182
|
proto = 'https' if request.is_secure() else 'http'
|
|
140
183
|
proto_host = f'{proto}://{request.get_host()}'
|
|
141
184
|
content = make_entry(proto_host, package)
|
|
142
185
|
return HttpResponse(f'{FEED_XML_PRE}\n{content}{FEED_XML_POST}\n' % {
|
|
143
186
|
'BASEURL': proto_host,
|
|
144
|
-
'UPDATED': datetime.now().isoformat()
|
|
187
|
+
'UPDATED': datetime.now(timezone.utc).isoformat()
|
|
145
188
|
},
|
|
146
189
|
content_type='application/xml')
|
|
147
190
|
return HttpResponseNotFound()
|
|
@@ -158,46 +201,48 @@ def fetch_package_file(request: HttpRequest, name: str, version: str) -> HttpRes
|
|
|
158
201
|
This also handles deletions. Deletions will only be allowed with authentication and with
|
|
159
202
|
``settings.ALLOW_PACKAGE_DELETION`` set to ``True``.
|
|
160
203
|
"""
|
|
161
|
-
if
|
|
204
|
+
if package := Package._default_manager.filter(nuget_id=name, version=version).first():
|
|
162
205
|
if request.method == 'GET':
|
|
163
206
|
with package.file.open('rb') as f:
|
|
164
207
|
package.download_count += 1
|
|
165
208
|
package.save()
|
|
166
209
|
return HttpResponse(f.read(), content_type='application/zip')
|
|
167
|
-
if request.method == 'DELETE' and settings.ALLOW_PACKAGE_DELETION:
|
|
210
|
+
if request.method == 'DELETE' and settings.ALLOW_PACKAGE_DELETION: # type: ignore[misc]
|
|
168
211
|
if not NugetUser.request_has_valid_token(request):
|
|
169
212
|
return JsonResponse({'error': 'Not authorized'}, status=403)
|
|
170
213
|
package.file.delete()
|
|
171
214
|
package.delete()
|
|
172
215
|
return HttpResponse(status=204)
|
|
173
|
-
|
|
174
|
-
return HttpResponse(status=405)
|
|
216
|
+
return HttpResponse(status=405)
|
|
175
217
|
return HttpResponseNotFound()
|
|
176
218
|
|
|
177
219
|
|
|
178
220
|
@method_decorator(csrf_exempt, name='dispatch')
|
|
179
221
|
class APIV2PackageView(View):
|
|
222
|
+
"""API V2 package upload view."""
|
|
223
|
+
@override
|
|
180
224
|
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
|
181
|
-
"""
|
|
225
|
+
"""Check if a user is authorised before allowing the request to continue."""
|
|
182
226
|
if not NugetUser.request_has_valid_token(request):
|
|
183
227
|
return JsonResponse({'error': 'Not authorized'}, status=403)
|
|
184
|
-
return super().dispatch(request, *args, **kwargs)
|
|
228
|
+
return cast('HttpResponse', super().dispatch(request, *args, **kwargs))
|
|
185
229
|
|
|
186
|
-
def put(self, request: HttpRequest) -> HttpResponse:
|
|
230
|
+
def put(self, request: HttpRequest) -> HttpResponse: # noqa: PLR6301
|
|
187
231
|
"""Upload a package. This must be a multipart upload with a single valid NuGet file."""
|
|
188
232
|
if not request.content_type or not request.content_type.startswith('multipart/'):
|
|
189
233
|
return JsonResponse(
|
|
190
234
|
{'error': f'Invalid content type: {request.content_type or "unknown"}'}, status=400)
|
|
191
235
|
try:
|
|
192
|
-
_, files = request.parse_file_upload(request.META, request)
|
|
236
|
+
_, files = request.parse_file_upload(request.META, BytesIO(request.body))
|
|
193
237
|
except MultiPartParserError:
|
|
194
238
|
return JsonResponse({'error': 'Invalid upload'}, status=400)
|
|
195
|
-
request.FILES.update(files)
|
|
239
|
+
request.FILES.update(cast('SupportsKeysAndGetItem[str, UploadedFile]', files))
|
|
196
240
|
if len(request.FILES) == 0:
|
|
197
241
|
return JsonResponse({'error': 'No files sent'}, status=400)
|
|
198
242
|
if len(request.FILES) > 1:
|
|
199
243
|
return JsonResponse({'error': 'More than one file sent'}, status=400)
|
|
200
|
-
nuget_file =
|
|
244
|
+
nuget_file = next(iter(request.FILES.values()))
|
|
245
|
+
assert not isinstance(nuget_file, list)
|
|
201
246
|
if not zipfile.is_zipfile(nuget_file):
|
|
202
247
|
return JsonResponse({'error': 'Not a zip file'}, status=400)
|
|
203
248
|
with zipfile.ZipFile(nuget_file) as z:
|
|
@@ -205,9 +250,8 @@ class APIV2PackageView(View):
|
|
|
205
250
|
if len(nuspecs) > 1 or not nuspecs:
|
|
206
251
|
return JsonResponse(
|
|
207
252
|
{
|
|
208
|
-
'error':
|
|
209
|
-
|
|
210
|
-
'found.'
|
|
253
|
+
'error': 'There should be exactly 1 nuspec file present. 0 or more than 1 '
|
|
254
|
+
'were found.'
|
|
211
255
|
},
|
|
212
256
|
status=400)
|
|
213
257
|
with TemporaryDirectory(suffix='.nuget-parse') as temp_dir:
|
|
@@ -216,34 +260,36 @@ class APIV2PackageView(View):
|
|
|
216
260
|
new_package = Package()
|
|
217
261
|
add_tags = []
|
|
218
262
|
add_authors = []
|
|
263
|
+
assert root is not None
|
|
264
|
+
metadata = root[0]
|
|
219
265
|
for key, column_name in NUSPEC_FIELD_MAPPINGS.items():
|
|
220
|
-
value =
|
|
221
|
-
|
|
222
|
-
if not value.text: # pragma no cover
|
|
266
|
+
value = tag_text_or(metadata.find(key, NUSPEC_NAMESPACES))
|
|
267
|
+
if not value: # pragma no cover
|
|
223
268
|
logger.warning('No value for key %s', key)
|
|
224
269
|
continue
|
|
225
|
-
column_type = (None if column_name not in PACKAGE_FIELDS else
|
|
226
|
-
|
|
270
|
+
column_type = (None if column_name not in PACKAGE_FIELDS else cast(
|
|
271
|
+
'Field[Any, Any] | ForeignObjectRel',
|
|
272
|
+
PACKAGE_FIELDS[column_name]).get_internal_type())
|
|
227
273
|
if not column_type or column_type == 'ManyToManyField':
|
|
228
274
|
if column_name == 'tags':
|
|
229
275
|
assert value is not None
|
|
230
|
-
tags = [x.strip() for x in re.split(r'\s+', value
|
|
276
|
+
tags = [x.strip() for x in re.split(r'\s+', value)]
|
|
231
277
|
for name in tags:
|
|
232
|
-
new_tag, _ = Tag.
|
|
278
|
+
new_tag, _ = Tag._default_manager.filter(name=name).get_or_create(name=name)
|
|
233
279
|
new_tag.save()
|
|
234
280
|
add_tags.append(new_tag)
|
|
235
281
|
elif column_name == 'authors':
|
|
236
|
-
authors = [x.strip() for x in
|
|
282
|
+
authors = [x.strip() for x in value.split(',')]
|
|
237
283
|
for name in authors:
|
|
238
|
-
new_author, _ = Author.
|
|
284
|
+
new_author, _ = Author._default_manager.get_or_create(name=name)
|
|
239
285
|
new_author.save()
|
|
240
286
|
add_authors.append(new_author)
|
|
241
287
|
else: # pragma no cover
|
|
242
288
|
logger.warning('Did not set %s', column_name)
|
|
243
289
|
elif column_type == 'BooleanField':
|
|
244
|
-
setattr(new_package, column_name, value.
|
|
290
|
+
setattr(new_package, column_name, value.lower() == 'true')
|
|
245
291
|
else:
|
|
246
|
-
setattr(new_package, column_name, value
|
|
292
|
+
setattr(new_package, column_name, value)
|
|
247
293
|
version_split = new_package.version.split('.')
|
|
248
294
|
new_package.version0 = int(version_split[0])
|
|
249
295
|
new_package.version1 = int(version_split[1])
|
|
@@ -252,9 +298,10 @@ class APIV2PackageView(View):
|
|
|
252
298
|
new_package.version3 = int(version_split[3])
|
|
253
299
|
except IndexError:
|
|
254
300
|
pass
|
|
255
|
-
new_package.size = nuget_file.size
|
|
256
|
-
new_package.file = File(nuget_file, nuget_file.name)
|
|
257
|
-
uploader = NugetUser.
|
|
301
|
+
new_package.size = cast('int', nuget_file.size)
|
|
302
|
+
new_package.file = File(nuget_file, nuget_file.name)
|
|
303
|
+
uploader = NugetUser._default_manager.filter(
|
|
304
|
+
token=request.headers['x-nuget-apikey']).first()
|
|
258
305
|
assert uploader is not None
|
|
259
306
|
new_package.uploader = uploader
|
|
260
307
|
try:
|
|
@@ -267,5 +314,5 @@ class APIV2PackageView(View):
|
|
|
267
314
|
return HttpResponse(status=201)
|
|
268
315
|
|
|
269
316
|
def post(self, request: HttpRequest) -> HttpResponse:
|
|
270
|
-
"""
|
|
317
|
+
"""``POST`` requests are treated the same as ``PUT``."""
|
|
271
318
|
return self.put(request)
|
minchoc/wsgi.py
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minchoc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal Chocolatey-compatible NuGet server in a Django app.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE.txt
|
|
7
|
+
Keywords: chocolatey,django,windows
|
|
8
|
+
Author: Andrew Udvare
|
|
9
|
+
Author-email: audvare@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Classifier: Environment :: Web Environment
|
|
21
|
+
Classifier: Intended Audience :: Information Technology
|
|
22
|
+
Classifier: Framework :: Django
|
|
23
|
+
Classifier: Intended Audience :: System Administrators
|
|
24
|
+
Classifier: Topic :: System :: Software Distribution
|
|
25
|
+
Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
|
|
26
|
+
Requires-Dist: django (>=5.2.8,<6.0.0)
|
|
27
|
+
Requires-Dist: django-stubs-ext (>=5.2.7,<6.0.0)
|
|
28
|
+
Requires-Dist: ply (>=3.11,<4.0)
|
|
29
|
+
Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
|
|
30
|
+
Project-URL: Documentation, https://minchoc.readthedocs.org
|
|
31
|
+
Project-URL: Homepage, https://tatsh.github.io/minchoc/
|
|
32
|
+
Project-URL: Issues, https://github.com/Tatsh/minchoc/issues
|
|
33
|
+
Project-URL: Repository, https://github.com/Tatsh/minchoc
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# minchoc
|
|
37
|
+
|
|
38
|
+
[](https://djangopackages.org/packages/p/minchoc/)
|
|
39
|
+
[](https://www.python.org/)
|
|
40
|
+
[](https://pypi.org/project/minchoc/)
|
|
41
|
+
[](https://github.com/Tatsh/minchoc/tags)
|
|
42
|
+
[](https://github.com/Tatsh/minchoc/blob/master/LICENSE.txt)
|
|
43
|
+
[](https://github.com/Tatsh/minchoc/compare/v0.1.0...master)
|
|
44
|
+
[](https://github.com/Tatsh/minchoc/actions/workflows/codeql.yml)
|
|
45
|
+
[](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml)
|
|
46
|
+
[](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml)
|
|
47
|
+
[](https://coveralls.io/github/Tatsh/minchoc?branch=master)
|
|
48
|
+
[](https://minchoc.readthedocs.org/?badge=latest)
|
|
49
|
+
[](https://www.djangoproject.com/)
|
|
50
|
+
[](http://mypy-lang.org/)
|
|
51
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
52
|
+
[](http://www.pydocstyle.org/en/stable/)
|
|
53
|
+
[](https://docs.pytest.org/en/stable/)
|
|
54
|
+
[](https://github.com/astral-sh/ruff)
|
|
55
|
+
[](https://pepy.tech/project/minchoc)
|
|
56
|
+
[](https://github.com/Tatsh/minchoc/stargazers)
|
|
57
|
+
|
|
58
|
+
[](https://bsky.app/profile/Tatsh.bsky.social)
|
|
59
|
+
[](https://hostux.social/@Tatsh)
|
|
60
|
+
|
|
61
|
+
**Min**imal **Choc**olatey-compatible NuGet server in a Django app.
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
pip install minchoc
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
In `settings.py`, add `'minchoc'` to `INSTALLED_APPS`. Set `ALLOW_PACKAGE_DELETION` to `True` if you
|
|
70
|
+
want to enable this API.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
INSTALLED_APPS = ['minchoc']
|
|
74
|
+
ALLOW_PACKAGE_DELETION = True
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A `DELETE` call to `/api/v2/package/<id>/<version>` will be denied even with authentication unless
|
|
78
|
+
`ALLOW_PACKAGE_DELETION` is set to `True`.
|
|
79
|
+
|
|
80
|
+
Add `path('/api/v2/', include('minchoc.urls'))` to your root `urls.py`. Example:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from django.urls import include, path
|
|
84
|
+
urlpatterns = [
|
|
85
|
+
path('admin/', admin.site.urls),
|
|
86
|
+
path('api/v2/', include('minchoc.urls')),
|
|
87
|
+
]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Run `./manage.py migrate` or similar to install the database schema.
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
When a user is created, a `NugetUser` is also made. This will contain the API key for pushing.
|
|
95
|
+
It can be viewed in admin.
|
|
96
|
+
|
|
97
|
+
### Add your source to Chocolatey
|
|
98
|
+
|
|
99
|
+
As administrator:
|
|
100
|
+
|
|
101
|
+
```shell
|
|
102
|
+
choco source add -s 'https://your-host/url-prefix'
|
|
103
|
+
choco apikey add -s 'https://your-host/url-prefix' -k 'your-key'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
On non-Windows platforms, you can use my [pychoco](https://github.com/Tatsh/pychoco) package, which
|
|
107
|
+
also supports the above commands.
|
|
108
|
+
|
|
109
|
+
### Supported commands
|
|
110
|
+
|
|
111
|
+
- `choco install`
|
|
112
|
+
- `choco push`
|
|
113
|
+
- `choco search`
|
|
114
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
minchoc/__init__.py,sha256=wTtVUM-_rSCfLo9zTOLmzFpMFs1ZzMLoXX2EcJjlW2c,137
|
|
2
|
+
minchoc/admin.py,sha256=F6zTKQrfMTJWlfM8ZzhOwLyLXAs2-ZLF0BCRcQTxCQQ,284
|
|
3
|
+
minchoc/apps.py,sha256=75RoQ70NrgHM030ZkAzvROBJaP5mfC3ZSAnd4Hp0b1Q,249
|
|
4
|
+
minchoc/constants.py,sha256=gibVWRLbNFN4Bc8gzDpWt3pc25f_DzYSxeQy4m97XYA,692
|
|
5
|
+
minchoc/filterlex.py,sha256=9XPz-IhHQdslcwx9iQX-tkz-3vdxJkm5wP4gucY7HX0,1258
|
|
6
|
+
minchoc/filteryacc.py,sha256=JBsEc5dNVszQnRYmIgnF_hCNqEn3UjnCRGA4IGQGJ38,3605
|
|
7
|
+
minchoc/migrations/0001_initial.py,sha256=AmhEzR46bpTqlArpV2-01rvMtA4lCU8pdRCfIfK9JC4,5706
|
|
8
|
+
minchoc/migrations/0002_alter_company_options_alter_package_unique_together_and_more.py,sha256=KCF9GRgmcxDW9TtoO2ee_e07WqMltv6HIkZ_MrpEQIw,966
|
|
9
|
+
minchoc/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
minchoc/models.py,sha256=b0s-sKSvVa2YWCArMDoRK49j2m2cI9-wnyogcjMtGyU,4487
|
|
11
|
+
minchoc/parsetab.py,sha256=sOmn4rzu8dvUU1Bcy912G3xZ_ipdkN7FWOHkQO_SWm0,6962
|
|
12
|
+
minchoc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
minchoc/urls.py,sha256=qk9uTDSgJA4SAuruuzKxkquNsmreDr9jHDpF_5Xidzk,588
|
|
14
|
+
minchoc/utils.py,sha256=HDzE4ZfqYpcW2p3Sw6dfM2wwHAAQBVh6odqSk8hsjjs,3879
|
|
15
|
+
minchoc/views.py,sha256=5PY3ihwz9CvEHpHUN854NZHhs9fyAyCTe9bTjE4AcRo,14356
|
|
16
|
+
minchoc/wsgi.py,sha256=TwtXfXExHDnzKDyx4_nYjfKDCkHYJC2dzLzKldrEzBk,194
|
|
17
|
+
minchoc-0.1.0.dist-info/METADATA,sha256=7BYDhXl3cL1AVBheoQ-3_OgU5sNujpE0VgXwGCkqaTU,5804
|
|
18
|
+
minchoc-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
19
|
+
minchoc-0.1.0.dist-info/licenses/LICENSE.txt,sha256=uwmPA5txPubVUZhsSPzZ8l1f89rPGDPR5qd8dzfnM0c,1082
|
|
20
|
+
minchoc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 minchoc authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
7
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
8
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
15
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
17
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
|
18
|
+
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 Human-Readable Project Name authors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|
minchoc-0.0.8.dist-info/METADATA
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: minchoc
|
|
3
|
-
Version: 0.0.8
|
|
4
|
-
Summary: Minimal Chocolatey-compatible NuGet server in a Django app.
|
|
5
|
-
Home-page: https://github.com/Tatsh/minchoc
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: chocolatey,django,windows
|
|
8
|
-
Author: Andrew Udvare
|
|
9
|
-
Author-email: audvare@gmail.com
|
|
10
|
-
Requires-Python: >=3.10,<4
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: Environment :: Web Environment
|
|
13
|
-
Classifier: Framework :: Django
|
|
14
|
-
Classifier: Framework :: Django :: 4.2
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Classifier: Programming Language :: Python
|
|
18
|
-
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Topic :: System :: Software Distribution
|
|
22
|
-
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
|
|
24
|
-
Requires-Dist: django (>=4.2.4,<5.0.0)
|
|
25
|
-
Requires-Dist: django-stubs-ext (>=4.2.2,<5.0.0)
|
|
26
|
-
Requires-Dist: ply (>=3.11,<4.0)
|
|
27
|
-
Project-URL: Documentation, https://minchoc.readthedocs.io/en/latest/
|
|
28
|
-
Project-URL: Repository, https://github.com/Tatsh/minchoc
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
|
|
31
|
-
# Minimal Chocolatey-compatible NuGet server in a Django app
|
|
32
|
-
|
|
33
|
-
[](https://minchoc.readthedocs.io/en/latest/?badge=latest)
|
|
34
|
-
[](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml)
|
|
35
|
-
[](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml)
|
|
36
|
-
[](https://coveralls.io/github/Tatsh/minchoc?branch=master)
|
|
37
|
-

|
|
38
|
-

|
|
39
|
-

|
|
40
|
-

|
|
41
|
-
|
|
42
|
-
## Installation
|
|
43
|
-
|
|
44
|
-
```shell
|
|
45
|
-
pip install minchoc
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
In `settings.py`, add `'minchoc'` to `INSTALLED_APPS`. Set `ALLOW_PACKAGE_DELETION` to `True` if you
|
|
49
|
-
want to enable this API.
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
INSTALLED_APPS = ['minchoc']
|
|
53
|
-
ALLOW_PACKAGE_DELETION = True
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Add `path('', include('minchoc.urls'))` to your root `urls.py`. Example:
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
from django.urls import include, path
|
|
60
|
-
urlpatterns = [
|
|
61
|
-
path('admin/', admin.site.urls),
|
|
62
|
-
path('', include('minchoc.urls')),
|
|
63
|
-
]
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
A `DELETE` call to `/api/v2/package/<id>/<version>` will be denied even with authentication unless
|
|
67
|
-
`ALLOW_PACKAGE_DELETION` is set to `True`.
|
|
68
|
-
|
|
69
|
-
## Notes
|
|
70
|
-
|
|
71
|
-
When a user is created, a `NugetUser` is also made. This will contain the API key for pushing.
|
|
72
|
-
It can be viewed in admin.
|
|
73
|
-
|
|
74
|
-
Only `choco install` and `choco push` are supported.
|
|
75
|
-
|
|
76
|
-
### Add source to Chocolatey
|
|
77
|
-
|
|
78
|
-
As administrator:
|
|
79
|
-
|
|
80
|
-
```shell
|
|
81
|
-
choco source add -s 'https://your-host/url-prefix'
|
|
82
|
-
choco apikey add -s 'https://your-host/url-prefix' -k 'your-key'
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
On non-Windows platforms, you can use my [pychoco](https://github.com/Tatsh/pychoco) package, which
|
|
86
|
-
also supports the above commands.
|
|
87
|
-
|
minchoc-0.0.8.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
minchoc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
minchoc/admin.py,sha256=mzMmYs8So6TPNUx6LZsDxGfCoLfdIb5xsGFM_7LCX4c,208
|
|
3
|
-
minchoc/apps.py,sha256=g4ijF3l6MIwTS5la3Z6Ru0LFkB0PgJvJ8Fkl64az-bM,143
|
|
4
|
-
minchoc/constants.py,sha256=0S-6sR-ojmKG750IHohqsP6zvKSDqodmcnTMT669p60,544
|
|
5
|
-
minchoc/filterlex.py,sha256=CK2rpOe8eHUl7lZpcgcIRXbXsUBB9BSiymozDSvu1qU,991
|
|
6
|
-
minchoc/filteryacc.py,sha256=Y66Q4MZnC5AQ8KsP--iaCkm-Xmqau21W86wp_OnMEt8,1376
|
|
7
|
-
minchoc/migrations/0001_initial.py,sha256=AmhEzR46bpTqlArpV2-01rvMtA4lCU8pdRCfIfK9JC4,5706
|
|
8
|
-
minchoc/migrations/0002_alter_company_options_alter_package_unique_together_and_more.py,sha256=zeo7cGCf3ZWcIzNBOSEM8FAv2CjE5IYTHuO_nfPVc8s,967
|
|
9
|
-
minchoc/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
minchoc/models.py,sha256=00Ul7AnX3672AiaQ7Bp3GDs9sYFhrRFmq7zYAwRoNNY,4345
|
|
11
|
-
minchoc/parsetab.py,sha256=LJTOdHZQ8QVdJeWgN_dYYre0NkGN_vBLHAh4l5VyGOE,2262
|
|
12
|
-
minchoc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
minchoc/urls.py,sha256=ICeCrhjD_QNeqvkXNUtJooO_j2NQyQFLpJuxEre0ilc,592
|
|
14
|
-
minchoc/utils.py,sha256=cTJtQAP2IlsiSCFlVMxnpPvCF5wfPmscoBd4dlHpGWE,3471
|
|
15
|
-
minchoc/views.py,sha256=aIp9X-thkKdWLwdeLlatsFLA1N26DlYInPpjWI9CS-g,12040
|
|
16
|
-
minchoc/wsgi.py,sha256=IqrBE5zgXD9vCOmobGUyQkr7NaUr_u0yThanG2sP9CE,134
|
|
17
|
-
minchoc-0.0.8.dist-info/LICENSE.txt,sha256=GI5chSxdE9Fi3wLZwiJOtFFJg_3eOkRrEuafX4zDd2Y,1102
|
|
18
|
-
minchoc-0.0.8.dist-info/METADATA,sha256=KWYPABR4PPaNPGm2ACANdAL54P6Dh1cR0o8Pgpt7HEg,3257
|
|
19
|
-
minchoc-0.0.8.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
|
|
20
|
-
minchoc-0.0.8.dist-info/RECORD,,
|