minchoc 0.0.11__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 CHANGED
@@ -1,4 +1,7 @@
1
+ """minchoc package."""
2
+ from __future__ import annotations
3
+
1
4
  import django_stubs_ext
2
5
 
3
6
  django_stubs_ext.monkeypatch()
4
- __version__ = '0.0.11'
7
+ __version__ = '0.1.0'
minchoc/admin.py CHANGED
@@ -1,3 +1,6 @@
1
+ """Django admin model registrations."""
2
+ from __future__ import annotations
3
+
1
4
  from django.contrib import admin
2
5
 
3
6
  from .models import Author, Company, NugetUser, Package
minchoc/apps.py CHANGED
@@ -1,6 +1,10 @@
1
+ """App configuration."""
2
+ from __future__ import annotations
3
+
1
4
  from django.apps import AppConfig
2
5
 
3
6
 
4
7
  class MainConfig(AppConfig):
8
+ """Configuration for the minchoc app."""
5
9
  default_auto_field = 'django.db.models.BigAutoField'
6
10
  name = 'minchoc'
minchoc/constants.py CHANGED
@@ -1,6 +1,9 @@
1
+ """Constants."""
2
+ from __future__ import annotations
3
+
1
4
  __all__ = ('FEED_XML_POST', 'FEED_XML_PRE')
2
5
 
3
- FEED_XML_PRE = '''<?xml version="1.0" encoding="utf-8" standalone="yes"?>
6
+ FEED_XML_PRE = """<?xml version="1.0" encoding="utf-8" standalone="yes"?>
4
7
  <feed xml:base="%(BASEURL)s/api/v1/"
5
8
  xmlns="http://www.w3.org/2005/Atom"
6
9
  xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
@@ -8,5 +11,15 @@ FEED_XML_PRE = '''<?xml version="1.0" encoding="utf-8" standalone="yes"?>
8
11
  <id>%(BASEURL)s/api/v1/Packages</id>
9
12
  <title type="text">Packages</title>
10
13
  <updated>%(UPDATED)s</updated>
11
- <link rel="self" title="Packages" href="Packages" />'''
14
+ <link rel="self" title="Packages" href="Packages" />"""
15
+ """
16
+ Feed XML preamble.
17
+
18
+ :meta hide-value:
19
+ """
12
20
  FEED_XML_POST = '</feed>'
21
+ """
22
+ Feed XML file suffix.
23
+
24
+ :meta hide-value:
25
+ """
minchoc/filterlex.py CHANGED
@@ -1,3 +1,7 @@
1
+ """Lexer."""
2
+ # ruff: noqa: D300,D400,N816
3
+ from __future__ import annotations
4
+
1
5
  from typing import Any
2
6
 
3
7
  from ply import lex
@@ -43,7 +47,7 @@ def t_string_escapedquote(_t: lex.LexToken) -> None:
43
47
  def t_string_quote(t: lex.LexToken) -> lex.LexToken:
44
48
  r"'"
45
49
  t.value = t.lexer.lexdata[t.lexer.code_start:t.lexer.lexpos - 1]
46
- t.type = "STRING"
50
+ t.type = 'STRING'
47
51
  t.lexer.lineno += t.value.count('\n')
48
52
  t.lexer.begin('INITIAL')
49
53
  return t
minchoc/filteryacc.py CHANGED
@@ -1,11 +1,17 @@
1
- from collections.abc import Sequence
2
- from typing import Any, Literal, cast
1
+ """Parser."""
2
+ # ruff: noqa: D205,D208,D209,D400,D403
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Literal, cast
3
6
 
4
7
  from django.db.models import Q
8
+ from minchoc.filterlex import tokens # noqa: F401
5
9
  from ply import yacc
6
- from ply.lex import LexToken
7
10
 
8
- from minchoc.filterlex import tokens # noqa: F401
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Sequence
13
+
14
+ from ply.lex import LexToken
9
15
 
10
16
  __all__ = ('FIELD_MAPPING', 'parser')
11
17
 
@@ -29,7 +35,7 @@ def p_substringof(p: yacc.YaccProduction) -> None:
29
35
  a: str
30
36
  b: Q
31
37
  _, __, ___, a, ____, b, _____ = p
32
- db_field = cast(Sequence[Any], b.children[0])[0]
38
+ db_field = cast('Sequence[Any]', b.children[0])[0]
33
39
  prefix = ''
34
40
  if '__iexact' in db_field:
35
41
  prefix = 'i'
@@ -62,7 +68,7 @@ def p_expression_op(p: yacc.YaccProduction) -> None:
62
68
  """expression : expression OR expression
63
69
  | expression AND expression
64
70
  | expression NE expression
65
- | expression EQ expression"""
71
+ | expression EQ expression""" # noqa: DOC501
66
72
  setup_p0(p)
67
73
  a: Q
68
74
  b: Q | str
@@ -75,8 +81,8 @@ def p_expression_op(p: yacc.YaccProduction) -> None:
75
81
  assert isinstance(b, Q)
76
82
  p[0] &= a | b
77
83
  else:
78
- db_field: str = cast(Sequence[Any], a.children[0])[0]
79
- if b == 'null' or (cast(Sequence[Any], b.children[0])[0]
84
+ db_field: str = cast('Sequence[Any]', a.children[0])[0]
85
+ if b == 'null' or (cast('Sequence[Any]', b.children[0])[0]
80
86
  if isinstance(b, Q) else None) == 'rhs__isnull':
81
87
  p[0] &= Q(**{f'{db_field}__isnull': op != 'ne'})
82
88
  else: # eq
@@ -94,7 +100,7 @@ def p_expression_str(p: yacc.YaccProduction) -> None:
94
100
 
95
101
 
96
102
  class GenericSyntaxError(SyntaxError):
97
- def __init__(self, index: int, token: str):
103
+ def __init__(self, index: int, token: str) -> None:
98
104
  super().__init__(f'Syntax error (index: {index}, token: "{token}")')
99
105
 
100
106
 
@@ -1,8 +1,8 @@
1
1
  # Generated by Django 4.2.4 on 2023-08-20 02:39
2
2
 
3
- import django.db.models.deletion
4
3
  from django.conf import settings
5
4
  from django.db import migrations, models
5
+ import django.db.models.deletion
6
6
 
7
7
 
8
8
  class Migration(migrations.Migration):
@@ -30,5 +30,5 @@ class Migration(migrations.Migration):
30
30
  model_name='package',
31
31
  name='version_download_count',
32
32
  ),
33
- migrations.DeleteModel(name='PackageVersionDownloadCount',),
33
+ migrations.DeleteModel(name='PackageVersionDownloadCount'),
34
34
  ]
minchoc/models.py CHANGED
@@ -1,26 +1,32 @@
1
+ """Model definitions."""
2
+ # ruff: noqa: D106, DJ001
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, cast
1
6
  import uuid
2
- from typing import Any, Self, cast
3
7
 
4
8
  from django.conf import settings
5
- from django.contrib.auth.models import AbstractUser
6
9
  from django.db import models
7
- from django.db.models.manager import BaseManager
8
10
  from django.db.models.signals import post_save
9
- from django.http import HttpRequest
11
+ from django_stubs_ext.db.models import TypedModelMeta
12
+ from typing_extensions import override
13
+
14
+ if TYPE_CHECKING:
15
+ from django.contrib.auth.models import AbstractUser
16
+ from django.http import HttpRequest
10
17
 
11
18
  __all__ = ('Author', 'Company', 'NugetUser', 'Package')
12
19
 
13
20
 
14
21
  class Company(models.Model):
15
22
  """Company associated to NuGet packages."""
16
- class Meta:
23
+ name = models.CharField(max_length=255, unique=True)
24
+
25
+ class Meta(TypedModelMeta):
17
26
  verbose_name = 'company'
18
27
  verbose_name_plural = 'companies'
19
28
 
20
- objects: BaseManager[Self]
21
-
22
- name = models.CharField(max_length=255, unique=True)
23
-
29
+ @override
24
30
  def __str__(self) -> str:
25
31
  return self.name
26
32
 
@@ -31,24 +37,26 @@ class NugetUser(models.Model):
31
37
  company = models.ForeignKey(Company, on_delete=models.CASCADE, null=True)
32
38
  token = models.UUIDField()
33
39
 
40
+ @override
41
+ def __str__(self) -> str:
42
+ return cast('str', self.base.username)
43
+
34
44
  @staticmethod
35
45
  def token_exists(token: str | None) -> bool:
36
- """Simple method to check if a token exists."""
46
+ """Check if a token exists."""
37
47
  return bool(token and NugetUser._default_manager.filter(token=token).exists())
38
48
 
39
49
  @staticmethod
40
50
  def request_has_valid_token(request: HttpRequest) -> bool:
41
- """
42
- Checks if the API key in the request (header ``X-NuGet-ApiKey``, case insensitive) is valid.
43
- """
51
+ """Check if the API key in the request is valid."""
44
52
  return NugetUser.token_exists(request.headers.get('X-NuGet-ApiKey'))
45
53
 
46
- def __str__(self) -> str:
47
- return cast(str, self.base.username)
48
-
49
54
 
50
- def post_save_receiver(sender: AbstractUser, instance: AbstractUser, **kwargs: Any) -> None:
51
- """Callback to create a ``NugetUser`` when a new user is saved."""
55
+ def post_save_receiver(
56
+ sender: AbstractUser, # noqa: ARG001
57
+ instance: AbstractUser,
58
+ **kwargs: Any) -> None: # noqa: ARG001
59
+ """Create a ``NugetUser`` when a new user is saved."""
52
60
  if not NugetUser._default_manager.filter(base=instance).exists():
53
61
  nuget_user = NugetUser()
54
62
  nuget_user.base = instance
@@ -63,6 +71,7 @@ class Author(models.Model):
63
71
  """Author of the software, not the NuGet spec."""
64
72
  name = models.CharField(max_length=255, unique=True)
65
73
 
74
+ @override
66
75
  def __str__(self) -> str:
67
76
  return self.name
68
77
 
@@ -71,24 +80,20 @@ class Tag(models.Model):
71
80
  """Tag associated to NuGet packages."""
72
81
  name = models.CharField(max_length=128, unique=True)
73
82
 
83
+ @override
74
84
  def __str__(self) -> str:
75
85
  return self.name
76
86
 
77
87
 
78
88
  class Package(models.Model):
79
89
  """An instance of a NuGet package."""
80
- class Meta:
81
- constraints = [
82
- models.UniqueConstraint(fields=('nuget_id', 'version'), name='id_and_version_uniq')
83
- ]
84
-
85
90
  authors = models.ManyToManyField(Author)
86
- copyright = models.TextField(null=True) # noqa: A003
91
+ copyright = models.TextField(null=True)
87
92
  dependencies = models.JSONField(null=True)
88
93
  description = models.TextField(null=True)
89
94
  download_count = models.PositiveBigIntegerField(default=0)
90
95
  file = models.FileField(upload_to='packages')
91
- hash = models.TextField(null=True) # noqa: A003
96
+ hash = models.TextField(null=True)
92
97
  hash_algorithm = models.CharField(max_length=32, null=True)
93
98
  icon_url = models.URLField(null=True)
94
99
  is_absolute_latest_version = models.BooleanField(default=True)
@@ -105,7 +110,7 @@ class Package(models.Model):
105
110
  size = models.PositiveIntegerField()
106
111
  source_url = models.URLField(null=True)
107
112
  summary = models.TextField(null=True)
108
- tags = models.ManyToManyField(Tag)
113
+ tags = models.ManyToManyField(Tag) # type: ignore[var-annotated]
109
114
  title = models.CharField(max_length=255)
110
115
  uploader = models.ForeignKey(NugetUser, on_delete=models.CASCADE)
111
116
  version = models.CharField(max_length=128)
@@ -115,5 +120,11 @@ class Package(models.Model):
115
120
  version3 = models.PositiveIntegerField(null=True)
116
121
  version_beta = models.CharField(max_length=128, null=True)
117
122
 
123
+ class Meta(TypedModelMeta):
124
+ constraints = [ # noqa: RUF012
125
+ models.UniqueConstraint(fields=('nuget_id', 'version'), name='id_and_version_uniq')
126
+ ]
127
+
128
+ @override
118
129
  def __str__(self) -> str:
119
130
  return f'{self.title} {self.version}'
minchoc/parsetab.py CHANGED
@@ -5,7 +5,7 @@ _tabversion = '3.10'
5
5
 
6
6
  _lr_method = 'LALR'
7
7
 
8
- _lr_signature = 'AND COMMA EQ FIELD ISLATESTVERSION LPAREN NE NULL OR RPAREN STRING SUBSTRINGOF TOLOWERexpression : LPAREN expression RPARENsubstringof : SUBSTRINGOF LPAREN STRING COMMA expression RPARENtolower : TOLOWER LPAREN FIELD RPARENexpression : FIELDexpression : expression OR expression\n | expression AND expression\n | expression NE expression\n | expression EQ expressionexpression : STRINGexpression : NULL\n | substringof\n | tolower\n | ISLATESTVERSION'
8
+ _lr_signature = 'AND COMMA EQ FIELD ISLATESTVERSION LPAREN NE NULL OR RPAREN STRING SUBSTRINGOF TOLOWERexpression : LPAREN expression RPARENsubstringof : SUBSTRINGOF LPAREN STRING COMMA expression RPARENtolower : TOLOWER LPAREN FIELD RPARENexpression : FIELDexpression : expression OR expression\n| expression AND expression\n| expression NE expression\n| expression EQ expressionexpression : STRINGexpression : NULL\n| substringof\n| tolower\n| ISLATESTVERSION'
9
9
 
10
10
  _lr_action_items = {
11
11
  'LPAREN': ([
@@ -418,22 +418,22 @@ del _lr_goto_items
418
418
  _lr_productions = [
419
419
  ("S' -> expression", "S'", 1, None, None, None),
420
420
  ('expression -> LPAREN expression RPAREN', 'expression', 3, 'p_expression_expr',
421
- 'filteryacc.py', 22),
421
+ 'filteryacc.py', 27),
422
422
  ('substringof -> SUBSTRINGOF LPAREN STRING COMMA expression RPAREN', 'substringof', 6,
423
- 'p_substringof', 'filteryacc.py', 28),
424
- ('tolower -> TOLOWER LPAREN FIELD RPAREN', 'tolower', 4, 'p_tolower', 'filteryacc.py', 40),
425
- ('expression -> FIELD', 'expression', 1, 'p_expression_field', 'filteryacc.py', 50),
423
+ 'p_substringof', 'filteryacc.py', 33),
424
+ ('tolower -> TOLOWER LPAREN FIELD RPAREN', 'tolower', 4, 'p_tolower', 'filteryacc.py', 47),
425
+ ('expression -> FIELD', 'expression', 1, 'p_expression_field', 'filteryacc.py', 55),
426
426
  ('expression -> expression OR expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
427
- 59),
427
+ 68),
428
428
  ('expression -> expression AND expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
429
- 60),
429
+ 69),
430
430
  ('expression -> expression NE expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
431
- 61),
431
+ 70),
432
432
  ('expression -> expression EQ expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
433
- 62),
434
- ('expression -> STRING', 'expression', 1, 'p_expression_str', 'filteryacc.py', 75),
435
- ('expression -> NULL', 'expression', 1, 'p_expression', 'filteryacc.py', 84),
436
- ('expression -> substringof', 'expression', 1, 'p_expression', 'filteryacc.py', 85),
437
- ('expression -> tolower', 'expression', 1, 'p_expression', 'filteryacc.py', 86),
438
- ('expression -> ISLATESTVERSION', 'expression', 1, 'p_expression', 'filteryacc.py', 87),
433
+ 71),
434
+ ('expression -> STRING', 'expression', 1, 'p_expression_str', 'filteryacc.py', 95),
435
+ ('expression -> NULL', 'expression', 1, 'p_expression', 'filteryacc.py', 108),
436
+ ('expression -> substringof', 'expression', 1, 'p_expression', 'filteryacc.py', 109),
437
+ ('expression -> tolower', 'expression', 1, 'p_expression', 'filteryacc.py', 110),
438
+ ('expression -> ISLATESTVERSION', 'expression', 1, 'p_expression', 'filteryacc.py', 111),
439
439
  ]
minchoc/urls.py CHANGED
@@ -1,3 +1,6 @@
1
+ """URL patterns."""
2
+ from __future__ import annotations
3
+
1
4
  from django.urls import path
2
5
 
3
6
  from . import views
@@ -10,9 +13,7 @@ urlpatterns = [
10
13
  path('FindPackagesById()', views.find_packages_by_id),
11
14
  path('Packages()', views.packages),
12
15
  path("Packages(Id='<name>',Version='<version>')", views.packages_with_args),
13
- path('api/v2/$metadata', views.metadata),
14
- path('api/v2/package/<name>/<version>', views.fetch_package_file),
15
- path('api/v2/package/', views.APIV2PackageView.as_view()),
16
- # path('api/v2/Search()', views.APIV2PackageView.as_view()), # noqa: ERA001
16
+ path('package/<name>/<version>', views.fetch_package_file),
17
+ path('package/', views.APIV2PackageView.as_view()),
17
18
  path('', views.home)
18
19
  ]
minchoc/utils.py CHANGED
@@ -1,17 +1,23 @@
1
- from xml.etree.ElementTree import Element
1
+ """Utility functions."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
2
5
 
3
6
  from django.db.models import Sum
4
7
 
5
8
  from .models import Package
6
9
 
10
+ if TYPE_CHECKING:
11
+ from xml.etree.ElementTree import Element # noqa: S405
12
+
7
13
  __all__ = ('make_entry', 'tag_text_or')
8
14
 
9
15
 
10
16
  def make_entry(host: str, package: Package, ending: str = '\n') -> str:
11
- """Creates a package ``<entry>`` element for a package XML feed."""
17
+ """Create a package ``<entry>`` element for a package XML feed."""
12
18
  total_downloads = Package._default_manager.filter(nuget_id=package.nuget_id).aggregate(
13
19
  total_downloads=Sum('download_count'))['total_downloads']
14
- return f'''<entry>
20
+ return f"""<entry>
15
21
  <id>{host}/api/v2/Packages(Id='{package.nuget_id}',Version='{package.version}')</id>
16
22
  <category term="NuGetGallery.V2FeedPackage"
17
23
  scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
@@ -55,7 +61,7 @@ def make_entry(host: str, package: Package, ending: str = '\n') -> str:
55
61
  <d:Version>{package.version}</d:Version>
56
62
  <d:VersionDownloadCount m:type="Edm.Int32">{package.download_count}</d:VersionDownloadCount>
57
63
  </m:properties>
58
- </entry>{ending}''' # noqa: E501
64
+ </entry>{ending}""" # noqa: E501
59
65
 
60
66
 
61
67
  def tag_text_or(tag: Element | None, default: str | None = None) -> str | None:
minchoc/views.py CHANGED
@@ -1,22 +1,26 @@
1
- import logging
2
- import re
3
- import zipfile
4
- from datetime import UTC, datetime
1
+ """Views."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime, timezone
5
+ from io import BytesIO
5
6
  from pathlib import Path
6
7
  from tempfile import TemporaryDirectory
7
8
  from typing import TYPE_CHECKING, Any, cast
9
+ import logging
10
+ import re
11
+ import zipfile
8
12
 
9
13
  from defusedxml.ElementTree import parse as parse_xml
10
14
  from django.conf import settings
11
15
  from django.core.files import File
12
16
  from django.db import IntegrityError
13
- from django.db.models import Field, ForeignObjectRel
14
17
  from django.http import HttpRequest, HttpResponse, HttpResponseNotFound, JsonResponse
15
18
  from django.http.multipartparser import MultiPartParserError
16
19
  from django.utils.decorators import method_decorator
17
20
  from django.views import View
18
21
  from django.views.decorators.csrf import csrf_exempt
19
22
  from django.views.decorators.http import require_http_methods
23
+ from typing_extensions import override
20
24
 
21
25
  from .constants import FEED_XML_POST, FEED_XML_PRE
22
26
  from .filteryacc import FIELD_MAPPING, parser as filter_parser
@@ -26,6 +30,7 @@ from .utils import make_entry, tag_text_or
26
30
  if TYPE_CHECKING: # pragma: no cover
27
31
  from _typeshed import SupportsKeysAndGetItem
28
32
  from django.core.files.uploadedfile import UploadedFile
33
+ from django.db.models import Field, ForeignObjectRel
29
34
 
30
35
  NUSPEC_NAMESPACES = {'': 'http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd'}
31
36
  NUSPEC_FIELD_AUTHORS = 'authors'
@@ -48,23 +53,23 @@ NUSPEC_FIELD_MAPPINGS = {
48
53
  NUSPEC_FIELD_SUMMARY: 'summary',
49
54
  NUSPEC_FIELD_TAGS: 'tags',
50
55
  NUSPEC_FIELD_TITLE: 'title',
51
- NUSPEC_FIELD_VERSION: 'version'
56
+ NUSPEC_FIELD_VERSION: 'version',
52
57
  }
53
- PACKAGE_FIELDS = {f.name: f for f in Package._meta.get_fields()} # noqa: SLF001
58
+ PACKAGE_FIELDS = {f.name: f for f in Package._meta.get_fields()}
54
59
 
55
60
  logger = logging.getLogger(__name__)
56
61
 
57
62
 
58
63
  @require_http_methods(['GET'])
59
64
  def home(_request: HttpRequest) -> HttpResponse:
60
- """Static homepage."""
65
+ """Get the content for the static homepage."""
61
66
  return JsonResponse({})
62
67
 
63
68
 
64
69
  @require_http_methods(['GET'])
65
70
  def metadata(_request: HttpRequest) -> HttpResponse:
66
- """Static page at ``/$metadata`` and at ``/api/v2/$metadata``."""
67
- 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"?>
68
73
  <service xml:base="http://fixme/api/v2/"
69
74
  xmlns:atom="http://www.w3.org/2005/Atom"
70
75
  xmlns:app="http://www.w3.org/2007/app"
@@ -73,30 +78,57 @@ def metadata(_request: HttpRequest) -> HttpResponse:
73
78
  <atom:title>Default</atom:title>
74
79
  <collection href="Packages"><atom:title>Packages</atom:title></collection>
75
80
  </workspace>
76
- </service>\n''',
81
+ </service>\n""",
77
82
  content_type='application/xml')
78
83
 
79
84
 
80
85
  @require_http_methods(['GET'])
81
86
  def find_packages_by_id(request: HttpRequest) -> HttpResponse:
82
87
  """
83
- Takes a ``GET`` request to find packages.
88
+ Take a ``GET`` request to find packages.
84
89
 
85
90
  Sample URL: ``/FindPackagesById()?id=package-name``
91
+
92
+ Supports ``$skiptoken`` parameter for pagination in the format:
93
+ ``$skiptoken='PackageName','Version'``.
86
94
  """
87
- # TODO Handle $skiptoken ``$skiptoken='Vivaldi','1.13.971.8-snapshot'`` (alternative way to
88
- # paginate)
89
- if (sem_ver_level := request.GET.get('semVerLevel')):
95
+ if sem_ver_level := request.GET.get('semVerLevel'):
90
96
  logger.warning('Ignoring semVerLevel=%s', sem_ver_level)
91
97
  proto = 'https' if request.is_secure() else 'http'
92
98
  proto_host = f'{proto}://{request.get_host()}'
93
99
  try:
94
- content = '\n'.join(
95
- make_entry(proto_host, x)
96
- for x in Package._default_manager.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)
97
129
  return HttpResponse(f'{FEED_XML_PRE}{content}{FEED_XML_POST}\n' % {
98
130
  'BASEURL': proto_host,
99
- 'UPDATED': datetime.now(UTC).isoformat()
131
+ 'UPDATED': datetime.now(timezone.utc).isoformat()
100
132
  },
101
133
  content_type='application/xml')
102
134
  except KeyError:
@@ -106,8 +138,10 @@ def find_packages_by_id(request: HttpRequest) -> HttpResponse:
106
138
  @require_http_methods(['GET'])
107
139
  def packages(request: HttpRequest) -> HttpResponse:
108
140
  """
109
- Takes a ``GET`` request to find packages. Query parameters ``$skip``, ``$top`` and
110
- ``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.
111
145
 
112
146
  Sample URL: ``/Packages()?$orderby=id&$filter=(tolower(Id) eq 'package-name') and IsLatestVersion&$skip=0&$top=1``
113
147
  """ # noqa: E501
@@ -115,16 +149,16 @@ def packages(request: HttpRequest) -> HttpResponse:
115
149
  req_order_by = request.GET.get('$orderby')
116
150
  order_by = (FIELD_MAPPING[req_order_by]
117
151
  if req_order_by and req_order_by in FIELD_MAPPING else 'nuget_id')
118
- if (sem_ver_level := request.GET.get('semVerLevel')):
152
+ if sem_ver_level := request.GET.get('semVerLevel'):
119
153
  logger.warning('Ignoring semVerLevel=%s', sem_ver_level)
120
- if (skip := request.GET.get('$skip')):
154
+ if skip := request.GET.get('$skip'):
121
155
  logger.warning('Ignoring $skip=%s', skip)
122
- if (top := request.GET.get('$top')):
156
+ if top := request.GET.get('$top'):
123
157
  logger.warning('Ignoring $top=%s', top)
124
158
  try:
125
159
  filters = filter_parser.parse(filter_) if filter_ else {}
126
160
  except SyntaxError:
127
- return JsonResponse({'error': 'Invalid syntax in filter'}, status=400)
161
+ return JsonResponse({'error': 'Invalid syntax in filter.'}, status=400)
128
162
  proto = 'https' if request.is_secure() else 'http'
129
163
  proto_host = f'{proto}://{request.get_host()}'
130
164
  content = '\n'.join(
@@ -132,7 +166,7 @@ def packages(request: HttpRequest) -> HttpResponse:
132
166
  for x in Package._default_manager.order_by(order_by).filter(filters)[0:20])
133
167
  return HttpResponse(f'{FEED_XML_PRE}\n{content}{FEED_XML_POST}\n' % {
134
168
  'BASEURL': proto_host,
135
- 'UPDATED': datetime.now(UTC).isoformat()
169
+ 'UPDATED': datetime.now(timezone.utc).isoformat()
136
170
  },
137
171
  content_type='application/xml')
138
172
 
@@ -144,13 +178,13 @@ def packages_with_args(request: HttpRequest, name: str, version: str) -> HttpRes
144
178
 
145
179
  Sample URL: ``/Packages(Id='name',Version='123.0.0')``
146
180
  """
147
- if (package := Package._default_manager.filter(nuget_id=name, version=version).first()):
181
+ if package := Package._default_manager.filter(nuget_id=name, version=version).first():
148
182
  proto = 'https' if request.is_secure() else 'http'
149
183
  proto_host = f'{proto}://{request.get_host()}'
150
184
  content = make_entry(proto_host, package)
151
185
  return HttpResponse(f'{FEED_XML_PRE}\n{content}{FEED_XML_POST}\n' % {
152
186
  'BASEURL': proto_host,
153
- 'UPDATED': datetime.now(UTC).isoformat()
187
+ 'UPDATED': datetime.now(timezone.utc).isoformat()
154
188
  },
155
189
  content_type='application/xml')
156
190
  return HttpResponseNotFound()
@@ -167,7 +201,7 @@ def fetch_package_file(request: HttpRequest, name: str, version: str) -> HttpRes
167
201
  This also handles deletions. Deletions will only be allowed with authentication and with
168
202
  ``settings.ALLOW_PACKAGE_DELETION`` set to ``True``.
169
203
  """
170
- if (package := Package._default_manager.filter(nuget_id=name, version=version).first()):
204
+ if package := Package._default_manager.filter(nuget_id=name, version=version).first():
171
205
  if request.method == 'GET':
172
206
  with package.file.open('rb') as f:
173
207
  package.download_count += 1
@@ -185,19 +219,21 @@ def fetch_package_file(request: HttpRequest, name: str, version: str) -> HttpRes
185
219
 
186
220
  @method_decorator(csrf_exempt, name='dispatch')
187
221
  class APIV2PackageView(View):
222
+ """API V2 package upload view."""
223
+ @override
188
224
  def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
189
- """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."""
190
226
  if not NugetUser.request_has_valid_token(request):
191
227
  return JsonResponse({'error': 'Not authorized'}, status=403)
192
- return cast(HttpResponse, super().dispatch(request, *args, **kwargs))
228
+ return cast('HttpResponse', super().dispatch(request, *args, **kwargs))
193
229
 
194
- def put(self, request: HttpRequest) -> HttpResponse:
230
+ def put(self, request: HttpRequest) -> HttpResponse: # noqa: PLR6301
195
231
  """Upload a package. This must be a multipart upload with a single valid NuGet file."""
196
232
  if not request.content_type or not request.content_type.startswith('multipart/'):
197
233
  return JsonResponse(
198
234
  {'error': f'Invalid content type: {request.content_type or "unknown"}'}, status=400)
199
235
  try:
200
- _, files = request.parse_file_upload(request.META, request)
236
+ _, files = request.parse_file_upload(request.META, BytesIO(request.body))
201
237
  except MultiPartParserError:
202
238
  return JsonResponse({'error': 'Invalid upload'}, status=400)
203
239
  request.FILES.update(cast('SupportsKeysAndGetItem[str, UploadedFile]', files))
@@ -205,7 +241,7 @@ class APIV2PackageView(View):
205
241
  return JsonResponse({'error': 'No files sent'}, status=400)
206
242
  if len(request.FILES) > 1:
207
243
  return JsonResponse({'error': 'More than one file sent'}, status=400)
208
- nuget_file = list(request.FILES.values())[0]
244
+ nuget_file = next(iter(request.FILES.values()))
209
245
  assert not isinstance(nuget_file, list)
210
246
  if not zipfile.is_zipfile(nuget_file):
211
247
  return JsonResponse({'error': 'Not a zip file'}, status=400)
@@ -214,9 +250,8 @@ class APIV2PackageView(View):
214
250
  if len(nuspecs) > 1 or not nuspecs:
215
251
  return JsonResponse(
216
252
  {
217
- 'error':
218
- 'There should be exactly 1 nuspec file present. 0 or more than 1 were '
219
- 'found.'
253
+ 'error': 'There should be exactly 1 nuspec file present. 0 or more than 1 '
254
+ 'were found.'
220
255
  },
221
256
  status=400)
222
257
  with TemporaryDirectory(suffix='.nuget-parse') as temp_dir:
@@ -225,6 +260,7 @@ class APIV2PackageView(View):
225
260
  new_package = Package()
226
261
  add_tags = []
227
262
  add_authors = []
263
+ assert root is not None
228
264
  metadata = root[0]
229
265
  for key, column_name in NUSPEC_FIELD_MAPPINGS.items():
230
266
  value = tag_text_or(metadata.find(key, NUSPEC_NAMESPACES))
@@ -232,8 +268,8 @@ class APIV2PackageView(View):
232
268
  logger.warning('No value for key %s', key)
233
269
  continue
234
270
  column_type = (None if column_name not in PACKAGE_FIELDS else cast(
235
- Field[Any, Any]
236
- | ForeignObjectRel, PACKAGE_FIELDS[column_name]).get_internal_type())
271
+ 'Field[Any, Any] | ForeignObjectRel',
272
+ PACKAGE_FIELDS[column_name]).get_internal_type())
237
273
  if not column_type or column_type == 'ManyToManyField':
238
274
  if column_name == 'tags':
239
275
  assert value is not None
@@ -243,7 +279,7 @@ class APIV2PackageView(View):
243
279
  new_tag.save()
244
280
  add_tags.append(new_tag)
245
281
  elif column_name == 'authors':
246
- authors = [x.strip() for x in re.split(',', value)]
282
+ authors = [x.strip() for x in value.split(',')]
247
283
  for name in authors:
248
284
  new_author, _ = Author._default_manager.get_or_create(name=name)
249
285
  new_author.save()
@@ -262,7 +298,7 @@ class APIV2PackageView(View):
262
298
  new_package.version3 = int(version_split[3])
263
299
  except IndexError:
264
300
  pass
265
- new_package.size = cast(int, nuget_file.size)
301
+ new_package.size = cast('int', nuget_file.size)
266
302
  new_package.file = File(nuget_file, nuget_file.name)
267
303
  uploader = NugetUser._default_manager.filter(
268
304
  token=request.headers['x-nuget-apikey']).first()
@@ -278,5 +314,5 @@ class APIV2PackageView(View):
278
314
  return HttpResponse(status=201)
279
315
 
280
316
  def post(self, request: HttpRequest) -> HttpResponse:
281
- """A ``POST`` request is treated the same as ``PUT``."""
317
+ """``POST`` requests are treated the same as ``PUT``."""
282
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.8.1
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,95 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: minchoc
3
- Version: 0.0.11
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.11,<4
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: Environment :: Web Environment
13
- Classifier: Framework :: Django
14
- Classifier: Framework :: Django :: 5.0
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.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Topic :: System :: Software Distribution
22
- Classifier: Typing :: Typed
23
- Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
24
- Requires-Dist: django (>=5.0,<6.0)
25
- Requires-Dist: django-stubs-ext (>=4.2.7,<5.0.0)
26
- Requires-Dist: ply (>=3.11,<4.0)
27
- Project-URL: Documentation, https://minchoc.readthedocs.io/
28
- Project-URL: Repository, https://github.com/Tatsh/minchoc
29
- Description-Content-Type: text/markdown
30
-
31
- # minchoc
32
-
33
- [![QA](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml)
34
- [![Tests](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml/badge.svg)](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml)
35
- [![Coverage Status](https://coveralls.io/repos/github/Tatsh/minchoc/badge.svg?branch=master)](https://coveralls.io/github/Tatsh/minchoc?branch=master)
36
- [![Documentation Status](https://readthedocs.org/projects/minchoc/badge/?version=latest)](https://minchoc.readthedocs.io/en/latest/?badge=latest)
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.11/master)
41
-
42
- **Min**imal **Choc**olatey-compatible NuGet server in a Django app.
43
-
44
- ## Installation
45
-
46
- ```shell
47
- pip install minchoc
48
- ```
49
-
50
- In `settings.py`, add `'minchoc'` to `INSTALLED_APPS`. Set `ALLOW_PACKAGE_DELETION` to `True` if you
51
- want to enable this API.
52
-
53
- ```python
54
- INSTALLED_APPS = ['minchoc']
55
- ALLOW_PACKAGE_DELETION = True
56
- ```
57
-
58
- A `DELETE` call to `/api/v2/package/<id>/<version>` will be denied even with authentication unless
59
- `ALLOW_PACKAGE_DELETION` is set to `True`.
60
-
61
- Add `path('', include('minchoc.urls'))` to your root `urls.py`. Example:
62
-
63
- ```python
64
- from django.urls import include, path
65
- urlpatterns = [
66
- path('admin/', admin.site.urls),
67
- path('', include('minchoc.urls')),
68
- ]
69
- ```
70
-
71
- Run `./manage.py migrate` or similar to install the database schema.
72
-
73
- ## Notes
74
-
75
- When a user is created, a `NugetUser` is also made. This will contain the API key for pushing.
76
- It can be viewed in admin.
77
-
78
- ### Add your source to Chocolatey
79
-
80
- As administrator:
81
-
82
- ```shell
83
- choco source add -s 'https://your-host/url-prefix'
84
- choco apikey add -s 'https://your-host/url-prefix' -k 'your-key'
85
- ```
86
-
87
- On non-Windows platforms, you can use my [pychoco](https://github.com/Tatsh/pychoco) package, which
88
- also supports the above commands.
89
-
90
- ### Supported commands
91
-
92
- - `choco install`
93
- - `choco push`
94
- - `choco search`
95
-
@@ -1,20 +0,0 @@
1
- minchoc/__init__.py,sha256=T2y4X3kdqutDyak8m2oEGzhrepAy_7zJ9X5mRjFyHqE,79
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=5y1U5PpQK7RyoJyI-SfxKouiiXR1XBp49KtEi-5Bg3k,1180
6
- minchoc/filteryacc.py,sha256=vNdb8kYgzf6J1j_1F7i2hPG8AJNwtyNxc0Cy_A79LQU,3444
7
- minchoc/migrations/0001_initial.py,sha256=FVYZ857qvxEMTAqQM6ZUJF9xbqYvAAelvt5nBTV08Bw,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=e5WIuosSlWsrVf8NaokGNFpHNTTvZpOg29kNITtCAMU,4266
11
- minchoc/parsetab.py,sha256=a9Cr80lRMtKMwQNbAABoGShAAT03IpXS2c6FNLO1QE8,7066
12
- minchoc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- minchoc/urls.py,sha256=jrChHsm5BtyguFqZABTlakjGxzTVOfKnqMRe9i17eMk,673
14
- minchoc/utils.py,sha256=ZqMvb4BBFdYWm2yZzvHceh95jsJiRr3M9vKvwHymX4Q,3749
15
- minchoc/views.py,sha256=EJDCaAJW09W3KmbqCbBcwguhXv2q0CgWr2wZP1aTN94,12541
16
- minchoc/wsgi.py,sha256=IqrBE5zgXD9vCOmobGUyQkr7NaUr_u0yThanG2sP9CE,134
17
- minchoc-0.0.11.dist-info/LICENSE.txt,sha256=GI5chSxdE9Fi3wLZwiJOtFFJg_3eOkRrEuafX4zDd2Y,1102
18
- minchoc-0.0.11.dist-info/METADATA,sha256=40_ANdWw_G62WlfuYDgiun9hjn-bcn8NymlV98ruR9k,3359
19
- minchoc-0.0.11.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
20
- minchoc-0.0.11.dist-info/RECORD,,