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 +4 -1
- minchoc/admin.py +3 -0
- minchoc/apps.py +4 -0
- minchoc/constants.py +15 -2
- minchoc/filterlex.py +5 -1
- minchoc/filteryacc.py +15 -9
- minchoc/migrations/0001_initial.py +1 -1
- minchoc/migrations/0002_alter_company_options_alter_package_unique_together_and_more.py +1 -1
- minchoc/models.py +37 -26
- minchoc/parsetab.py +14 -14
- minchoc/urls.py +5 -4
- minchoc/utils.py +10 -4
- minchoc/views.py +78 -42
- 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.11.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.11.dist-info/LICENSE.txt +0 -21
- minchoc-0.0.11.dist-info/METADATA +0 -95
- minchoc-0.0.11.dist-info/RECORD +0 -20
minchoc/__init__.py
CHANGED
minchoc/admin.py
CHANGED
minchoc/apps.py
CHANGED
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 =
|
|
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 =
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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(
|
|
51
|
-
|
|
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)
|
|
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)
|
|
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
|
|
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',
|
|
421
|
+
'filteryacc.py', 27),
|
|
422
422
|
('substringof -> SUBSTRINGOF LPAREN STRING COMMA expression RPAREN', 'substringof', 6,
|
|
423
|
-
'p_substringof', 'filteryacc.py',
|
|
424
|
-
('tolower -> TOLOWER LPAREN FIELD RPAREN', 'tolower', 4, 'p_tolower', 'filteryacc.py',
|
|
425
|
-
('expression -> FIELD', 'expression', 1, 'p_expression_field', 'filteryacc.py',
|
|
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
|
-
|
|
427
|
+
68),
|
|
428
428
|
('expression -> expression AND expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
|
|
429
|
-
|
|
429
|
+
69),
|
|
430
430
|
('expression -> expression NE expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
|
|
431
|
-
|
|
431
|
+
70),
|
|
432
432
|
('expression -> expression EQ expression', 'expression', 3, 'p_expression_op', 'filteryacc.py',
|
|
433
|
-
|
|
434
|
-
('expression -> STRING', 'expression', 1, 'p_expression_str', 'filteryacc.py',
|
|
435
|
-
('expression -> NULL', 'expression', 1, 'p_expression', 'filteryacc.py',
|
|
436
|
-
('expression -> substringof', 'expression', 1, 'p_expression', 'filteryacc.py',
|
|
437
|
-
('expression -> tolower', 'expression', 1, 'p_expression', 'filteryacc.py',
|
|
438
|
-
('expression -> ISLATESTVERSION', 'expression', 1, 'p_expression', 'filteryacc.py',
|
|
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('
|
|
14
|
-
path('
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
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}
|
|
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
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
from datetime import
|
|
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()}
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
67
|
-
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"?>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
152
|
+
if sem_ver_level := request.GET.get('semVerLevel'):
|
|
119
153
|
logger.warning('Ignoring semVerLevel=%s', sem_ver_level)
|
|
120
|
-
if
|
|
154
|
+
if skip := request.GET.get('$skip'):
|
|
121
155
|
logger.warning('Ignoring $skip=%s', skip)
|
|
122
|
-
if
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
"""
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
317
|
+
"""``POST`` requests are treated the same as ``PUT``."""
|
|
282
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.
|
|
@@ -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
|
-
[](https://github.com/Tatsh/minchoc/actions/workflows/qa.yml)
|
|
34
|
-
[](https://github.com/Tatsh/minchoc/actions/workflows/tests.yml)
|
|
35
|
-
[](https://coveralls.io/github/Tatsh/minchoc?branch=master)
|
|
36
|
-
[](https://minchoc.readthedocs.io/en/latest/?badge=latest)
|
|
37
|
-

|
|
38
|
-

|
|
39
|
-

|
|
40
|
-

|
|
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
|
-
|
minchoc-0.0.11.dist-info/RECORD
DELETED
|
@@ -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,,
|