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/views.py CHANGED
@@ -1,7 +1,11 @@
1
- from datetime import datetime
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
- NUSPEC_XSD_URI_PREFIX = '{http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd}'
26
- NUSPEC_FIELD_AUTHORS = f'{NUSPEC_XSD_URI_PREFIX}authors'
27
- NUSPEC_FIELD_DESCRIPTION = f'{NUSPEC_XSD_URI_PREFIX}description'
28
- NUSPEC_FIELD_ID = f'{NUSPEC_XSD_URI_PREFIX}id'
29
- NUSPEC_FIELD_PROJECT_URL = f'{NUSPEC_XSD_URI_PREFIX}projectUrl'
30
- NUSPEC_FIELD_REQUIRE_LICENSE_ACCEPTANCE = f'{NUSPEC_XSD_URI_PREFIX}requireLicenseAcceptance'
31
- NUSPEC_FIELD_SOURCE_URL = f'{NUSPEC_XSD_URI_PREFIX}packageSourceUrl'
32
- NUSPEC_FIELD_SUMMARY = f'{NUSPEC_XSD_URI_PREFIX}summary'
33
- NUSPEC_FIELD_TAGS = f'{NUSPEC_XSD_URI_PREFIX}tags'
34
- NUSPEC_FIELD_TITLE = f'{NUSPEC_XSD_URI_PREFIX}title'
35
- NUSPEC_FIELD_VERSION = f'{NUSPEC_XSD_URI_PREFIX}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
- """Static homepage."""
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
- """Static page at ``/$metadata`` and at ``/api/v2/$metadata``."""
62
- return HttpResponse('''<?xml version="1.0" encoding="utf-8" standalone="yes"?>
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
- Takes a ``GET`` request to find packages.
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 (sem_ver_level := request.GET.get('semVerLevel')):
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
- content = '\n'.join(
88
- make_entry(proto_host, x)
89
- for x in Package.objects.filter(nuget_id=request.GET['id'].replace('\'', '')))
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
- Takes a ``GET`` request to find packages. Query parameters ``$skip``, ``$top`` and
103
- ``semVerLevel`` are ignored. This means pagination is currently not supported.
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
- order_by = request.GET.get('$orderby') or 'id'
109
- if (sem_ver_level := request.GET.get('semVerLevel')):
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 (skip := request.GET.get('$skip')):
154
+ if skip := request.GET.get('$skip'):
112
155
  logger.warning('Ignoring $skip=%s', skip)
113
- if (top := request.GET.get('$top')):
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.objects.order_by(order_by).filter(**filters)[0:20])
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 (package := Package.objects.filter(nuget_id=name, version=version).first()):
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 (package := Package.objects.filter(nuget_id=name, version=version).first()):
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
- else:
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
- """Checks if a user is authorised before allowing the request to continue."""
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 = list(request.FILES.values())[0]
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
- 'There should be exactly 1 nuspec file present. 0 or more than 1 were '
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 = root[0].find(key)
221
- assert value is not None
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
- PACKAGE_FIELDS[column_name].get_internal_type())
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.text)]
276
+ tags = [x.strip() for x in re.split(r'\s+', value)]
231
277
  for name in tags:
232
- new_tag, _ = Tag.objects.filter(name=name).get_or_create(name=name)
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 re.split(',', value.text)]
282
+ authors = [x.strip() for x in value.split(',')]
237
283
  for name in authors:
238
- new_author, _ = Author.objects.get_or_create(name=name)
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.text.lower() == 'true')
290
+ setattr(new_package, column_name, value.lower() == 'true')
245
291
  else:
246
- setattr(new_package, column_name, value.text)
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) # type: ignore[assignment]
257
- uploader = NugetUser.objects.filter(token=request.headers['x-nuget-apikey']).first()
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
- """A ``POST`` request is treated the same as ``PUT``."""
317
+ """``POST`` requests are treated the same as ``PUT``."""
271
318
  return self.put(request)
minchoc/wsgi.py CHANGED
@@ -1,4 +1,7 @@
1
+ """WSGI application."""
1
2
  # pragma no cover
3
+ from __future__ import annotations
4
+
2
5
  from django.core.wsgi import get_wsgi_application
3
6
 
4
7
  __all__ = ('application',)
@@ -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
+ [![Published on Django Packages](https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26)](https://djangopackages.org/packages/p/minchoc/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/minchoc.svg?color=blue&logo=python&logoColor=white)](https://www.python.org/)
40
+ [![PyPI - Version](https://img.shields.io/pypi/v/minchoc)](https://pypi.org/project/minchoc/)
41
+ [![GitHub tag (with filter)](https://img.shields.io/github/v/tag/Tatsh/minchoc)](https://github.com/Tatsh/minchoc/tags)
42
+ [![License](https://img.shields.io/github/license/Tatsh/minchoc)](https://github.com/Tatsh/minchoc/blob/master/LICENSE.txt)
43
+ [![GitHub commits since latest release (by SemVer including pre-releases)](https://img.shields.io/github/commits-since/Tatsh/minchoc/v0.1.0/master)](https://github.com/Tatsh/minchoc/compare/v0.1.0...master)
44
+ [![CodeQL](https://github.com/Tatsh/minchoc/actions/workflows/codeql.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/codeql.yml)
45
+ [![QA](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml)
46
+ [![Tests](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml)
47
+ [![Coverage Status](https://coveralls.io/repos/github/Tatsh/minchoc/badge.svg?branch=master)](https://coveralls.io/github/Tatsh/minchoc?branch=master)
48
+ [![Documentation Status](https://readthedocs.org/projects/minchoc/badge/?version=latest)](https://minchoc.readthedocs.org/?badge=latest)
49
+ [![Django](https://img.shields.io/badge/Django-092E20?logo=django&logoColor=green)](https://www.djangoproject.com/)
50
+ [![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
51
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
52
+ [![pydocstyle](https://img.shields.io/badge/pydocstyle-enabled-AD4CD3)](http://www.pydocstyle.org/en/stable/)
53
+ [![pytest](https://img.shields.io/badge/pytest-zz?logo=Pytest&labelColor=black&color=black)](https://docs.pytest.org/en/stable/)
54
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
55
+ [![Downloads](https://static.pepy.tech/badge/minchoc/month)](https://pepy.tech/project/minchoc)
56
+ [![Stargazers](https://img.shields.io/github/stars/Tatsh/minchoc?logo=github&style=flat)](https://github.com/Tatsh/minchoc/stargazers)
57
+
58
+ [![@Tatsh](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fpublic.api.bsky.app%2Fxrpc%2Fapp.bsky.actor.getProfile%2F%3Factor%3Ddid%3Aplc%3Auq42idtvuccnmtl57nsucz72%26query%3D%24.followersCount%26style%3Dsocial%26logo%3Dbluesky%26label%3DFollow%2520%40Tatsh&query=%24.followersCount&style=social&logo=bluesky&label=Follow%20%40Tatsh)](https://bsky.app/profile/Tatsh.bsky.social)
59
+ [![Mastodon Follow](https://img.shields.io/mastodon/follow/109370961877277568?domain=hostux.social&style=social)](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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.7.0
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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.
@@ -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
- [![Documentation Status](https://readthedocs.org/projects/minchoc/badge/?version=latest)](https://minchoc.readthedocs.io/en/latest/?badge=latest)
34
- [![QA](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml)
35
- [![Tests](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml)
36
- [![Coverage Status](https://coveralls.io/repos/github/Tatsh/minchoc/badge.svg?branch=master)](https://coveralls.io/github/Tatsh/minchoc?branch=master)
37
- ![PyPI - Version](https://img.shields.io/pypi/v/minchoc)
38
- ![GitHub tag (with filter)](https://img.shields.io/github/v/tag/Tatsh/minchoc)
39
- ![GitHub](https://img.shields.io/github/license/Tatsh/minchoc)
40
- ![GitHub commits since latest release (by SemVer including pre-releases)](https://img.shields.io/github/commits-since/Tatsh/minchoc/v0.0.8/master)
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
-
@@ -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,,