minchoc 0.0.8__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of minchoc might be problematic. Click here for more details.

minchoc/__init__.py CHANGED
@@ -0,0 +1,7 @@
1
+ """minchoc package."""
2
+ from __future__ import annotations
3
+
4
+ import django_stubs_ext
5
+
6
+ django_stubs_ext.monkeypatch()
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
@@ -5,14 +9,21 @@ from ply import lex
5
9
  __all__ = ('tokens',)
6
10
 
7
11
  states = (('string', 'exclusive'),)
8
- tokens = ('AND', 'EQ', 'ISLATESTVERSION', 'LPAREN', 'RPAREN', 'STRING', 'TOLOWER_ID')
12
+ tokens = ('AND', 'COMMA', 'EQ', 'FIELD', 'ISLATESTVERSION', 'LPAREN', 'NE', 'NULL', 'OR', 'RPAREN',
13
+ 'STRING', 'SUBSTRINGOF', 'TOLOWER')
9
14
 
10
15
  t_AND = r'and'
16
+ t_COMMA = r','
11
17
  t_EQ = r'eq'
18
+ t_FIELD = r'Description|Id|Tags'
12
19
  t_ISLATESTVERSION = r'IsLatestVersion'
13
- t_TOLOWER_ID = r'tolower\(Id\)'
14
20
  t_LPAREN = r'\('
21
+ t_NE = r'ne'
22
+ t_NULL = r'null'
23
+ t_OR = r'or'
15
24
  t_RPAREN = r'\)'
25
+ t_SUBSTRINGOF = r'substringof'
26
+ t_TOLOWER = r'tolower'
16
27
 
17
28
  t_ignore = ' '
18
29
  t_string_ignore = ' '
@@ -36,14 +47,14 @@ def t_string_escapedquote(_t: lex.LexToken) -> None:
36
47
  def t_string_quote(t: lex.LexToken) -> lex.LexToken:
37
48
  r"'"
38
49
  t.value = t.lexer.lexdata[t.lexer.code_start:t.lexer.lexpos - 1]
39
- t.type = "STRING"
50
+ t.type = 'STRING'
40
51
  t.lexer.lineno += t.value.count('\n')
41
52
  t.lexer.begin('INITIAL')
42
53
  return t
43
54
 
44
55
 
45
56
  def t_string_error(t: lex.LexToken) -> None:
46
- # print("illegal character '%s'" % t.value[0])
57
+ # print("illegal character '%s'" % t.value[0]) # noqa: ERA001
47
58
  t.lexer.skip(1)
48
59
 
49
60
 
minchoc/filteryacc.py CHANGED
@@ -1,56 +1,128 @@
1
- from typing import Any, cast
1
+ """Parser."""
2
+ # ruff: noqa: D205,D208,D209,D400,D403
3
+ from __future__ import annotations
2
4
 
3
- from ply import yacc
4
- from ply.lex import LexToken
5
+ from typing import TYPE_CHECKING, Any, Literal, cast
5
6
 
7
+ from django.db.models import Q
6
8
  from minchoc.filterlex import tokens # noqa: F401
9
+ from ply import yacc
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Sequence
13
+
14
+ from ply.lex import LexToken
15
+
16
+ __all__ = ('FIELD_MAPPING', 'parser')
17
+
18
+ FIELD_MAPPING = {'Description': 'description', 'Id': 'nuget_id', 'Tags': 'tags__name'}
19
+
20
+
21
+ def setup_p0(p: yacc.YaccProduction) -> None:
22
+ if not isinstance(p[0], Q): # pragma no cover
23
+ p[0] = Q()
24
+
25
+
26
+ def p_expression_expr(p: yacc.YaccProduction) -> None:
27
+ """expression : LPAREN expression RPAREN"""
28
+ setup_p0(p)
29
+ p[0] = p[2]
30
+
31
+
32
+ def p_substringof(p: yacc.YaccProduction) -> None:
33
+ """substringof : SUBSTRINGOF LPAREN STRING COMMA expression RPAREN"""
34
+ setup_p0(p)
35
+ a: str
36
+ b: Q
37
+ _, __, ___, a, ____, b, _____ = p
38
+ db_field = cast('Sequence[Any]', b.children[0])[0]
39
+ prefix = ''
40
+ if '__iexact' in db_field:
41
+ prefix = 'i'
42
+ db_field = db_field.replace('__iexact', '')
43
+ p[0] &= Q(**{f'{db_field}__{prefix}contains': a})
44
+
45
+
46
+ def p_tolower(p: yacc.YaccProduction) -> None:
47
+ """tolower : TOLOWER LPAREN FIELD RPAREN"""
48
+ setup_p0(p)
49
+ field: Literal['Description', 'Id', 'Tags']
50
+ _, __, ___, field, ____ = p
51
+ p[0] &= Q(**{f'{FIELD_MAPPING[field]}__iexact': None})
52
+
53
+
54
+ def p_expression_field(p: yacc.YaccProduction) -> None:
55
+ """expression : FIELD"""
56
+ setup_p0(p)
57
+ field: Literal['Description', 'Id', 'Tags']
58
+ _, field = p
59
+ p[0] &= Q(**{FIELD_MAPPING[field]: None})
60
+
61
+
62
+ class InvalidTypeForEq(Exception):
63
+ def __init__(self) -> None:
64
+ super().__init__('Only numbers and strings can be used with eq.')
65
+
66
+
67
+ def p_expression_op(p: yacc.YaccProduction) -> None:
68
+ """expression : expression OR expression
69
+ | expression AND expression
70
+ | expression NE expression
71
+ | expression EQ expression""" # noqa: DOC501
72
+ setup_p0(p)
73
+ a: Q
74
+ b: Q | str
75
+ op: Literal['and', 'eq', 'ne', 'or']
76
+ _, a, op, b = p
77
+ if op == 'and':
78
+ assert isinstance(b, Q)
79
+ p[0] &= a & b
80
+ elif op == 'or':
81
+ assert isinstance(b, Q)
82
+ p[0] &= a | b
83
+ else:
84
+ db_field: str = cast('Sequence[Any]', a.children[0])[0]
85
+ if b == 'null' or (cast('Sequence[Any]', b.children[0])[0]
86
+ if isinstance(b, Q) else None) == 'rhs__isnull':
87
+ p[0] &= Q(**{f'{db_field}__isnull': op != 'ne'})
88
+ else: # eq
89
+ if not isinstance(b, int | str):
90
+ raise InvalidTypeForEq
91
+ p[0] &= Q(**{db_field: b})
92
+
7
93
 
8
- __all__ = ('parser',)
94
+ def p_expression_str(p: yacc.YaccProduction) -> None:
95
+ """expression : STRING"""
96
+ setup_p0(p)
97
+ s: str
98
+ _, s = p
99
+ p[0] = s
9
100
 
10
101
 
11
- def dict_if_not_dict(p: yacc.YaccProduction, index: int) -> dict[Any, Any]:
12
- try:
13
- if not isinstance(p[index], dict):
14
- return {}
15
- return cast(dict[Any, Any], p[index])
16
- except IndexError:
17
- return {}
102
+ class GenericSyntaxError(SyntaxError):
103
+ def __init__(self, index: int, token: str) -> None:
104
+ super().__init__(f'Syntax error (index: {index}, token: "{token}")')
18
105
 
19
106
 
20
107
  def p_expression(p: yacc.YaccProduction) -> None:
21
- """
22
- expression : expression AND expression
23
- | LPAREN expression RPAREN
24
- """
25
- p[0] = {
26
- **dict_if_not_dict(p, 0),
27
- **dict_if_not_dict(p, 1),
28
- **dict_if_not_dict(p, 2),
29
- **dict_if_not_dict(p, 3)
30
- }
31
-
32
-
33
- def p_islatestversion(p: yacc.YaccProduction) -> None:
34
- """expression : ISLATESTVERSION"""
35
- p[0] = {
36
- **dict_if_not_dict(p, 0),
37
- **dict_if_not_dict(p, 1),
38
- **dict_if_not_dict(p, 2), 'is_latest_version': True
39
- }
40
-
41
-
42
- def p_tolower_id_eq_package(p: yacc.YaccProduction) -> None:
43
- """expression : TOLOWER_ID EQ STRING"""
44
- p[0] = {
45
- **dict_if_not_dict(p, 0),
46
- **dict_if_not_dict(p, 1),
47
- **dict_if_not_dict(p, 2), 'nuget_id__iexact': p[3]
48
- }
108
+ """expression : NULL
109
+ | substringof
110
+ | tolower
111
+ | ISLATESTVERSION"""
112
+ setup_p0(p)
113
+ expr: Any
114
+ _, expr = p
115
+ if expr == 'IsLatestVersion':
116
+ p[0] &= Q(is_latest_version=True)
117
+ elif expr == 'null':
118
+ p[0] &= Q(rhs__isnull=True)
119
+ else:
120
+ p[0] &= expr
49
121
 
50
122
 
51
123
  def p_error(p: LexToken) -> None:
52
- raise SyntaxError('Syntax error in input')
124
+ raise GenericSyntaxError(p.lexer.lexpos, p.value)
53
125
 
54
126
 
55
- #: An extremely basic parser for parsing ``$filter`` strings.
56
127
  parser = yacc.yacc(debug=False)
128
+ """An extremely basic parser for parsing ``$filter`` strings. Returns a ``Q`` instance."""
@@ -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,56 +1,63 @@
1
- from typing import Any, cast
1
+ """Model definitions."""
2
+ # ruff: noqa: D106, DJ001
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, cast
2
6
  import uuid
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
10
  from django.db.models.signals import post_save
8
- from django.http import HttpRequest
9
- import django_stubs_ext
11
+ from django_stubs_ext.db.models import TypedModelMeta
12
+ from typing_extensions import override
10
13
 
11
- django_stubs_ext.monkeypatch()
14
+ if TYPE_CHECKING:
15
+ from django.contrib.auth.models import AbstractUser
16
+ from django.http import HttpRequest
12
17
 
13
18
  __all__ = ('Author', 'Company', 'NugetUser', 'Package')
14
19
 
15
20
 
16
21
  class Company(models.Model):
17
22
  """Company associated to NuGet packages."""
18
- class Meta:
23
+ name = models.CharField(max_length=255, unique=True)
24
+
25
+ class Meta(TypedModelMeta):
19
26
  verbose_name = 'company'
20
27
  verbose_name_plural = 'companies'
21
28
 
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
 
27
33
 
28
34
  class NugetUser(models.Model):
29
35
  """An owner of a NuGet spec."""
30
- base = models.OneToOneField(settings.AUTH_USER_MODEL,
31
- on_delete=models.CASCADE) # type: ignore[var-annotated]
36
+ base = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
32
37
  company = models.ForeignKey(Company, on_delete=models.CASCADE, null=True)
33
38
  token = models.UUIDField()
34
39
 
40
+ @override
41
+ def __str__(self) -> str:
42
+ return cast('str', self.base.username)
43
+
35
44
  @staticmethod
36
45
  def token_exists(token: str | None) -> bool:
37
- """Simple method to check if a token exists."""
38
- return bool(token and NugetUser.objects.filter(token=token).exists())
46
+ """Check if a token exists."""
47
+ return bool(token and NugetUser._default_manager.filter(token=token).exists())
39
48
 
40
49
  @staticmethod
41
50
  def request_has_valid_token(request: HttpRequest) -> bool:
42
- """
43
- Checks if the API key in the request (header ``X-NuGet-ApiKey``, case insensitive) is valid.
44
- """
51
+ """Check if the API key in the request is valid."""
45
52
  return NugetUser.token_exists(request.headers.get('X-NuGet-ApiKey'))
46
53
 
47
- def __str__(self) -> str:
48
- return cast(str, self.base.username)
49
54
 
50
-
51
- def post_save_receiver(sender: AbstractUser, instance: AbstractUser, **kwargs: Any) -> None:
52
- """Callback to create a ``NugetUser`` when a new user is saved."""
53
- if not NugetUser.objects.filter(base=instance).exists():
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."""
60
+ if not NugetUser._default_manager.filter(base=instance).exists():
54
61
  nuget_user = NugetUser()
55
62
  nuget_user.base = instance
56
63
  nuget_user.token = uuid.uuid4()
@@ -64,6 +71,7 @@ class Author(models.Model):
64
71
  """Author of the software, not the NuGet spec."""
65
72
  name = models.CharField(max_length=255, unique=True)
66
73
 
74
+ @override
67
75
  def __str__(self) -> str:
68
76
  return self.name
69
77
 
@@ -72,18 +80,14 @@ class Tag(models.Model):
72
80
  """Tag associated to NuGet packages."""
73
81
  name = models.CharField(max_length=128, unique=True)
74
82
 
83
+ @override
75
84
  def __str__(self) -> str:
76
85
  return self.name
77
86
 
78
87
 
79
88
  class Package(models.Model):
80
89
  """An instance of a NuGet package."""
81
- class Meta:
82
- constraints = [
83
- models.UniqueConstraint(fields=('nuget_id', 'version'), name='id_and_version_uniq')
84
- ]
85
-
86
- authors = models.ManyToManyField(Author) # type: ignore[var-annotated]
90
+ authors = models.ManyToManyField(Author)
87
91
  copyright = models.TextField(null=True)
88
92
  dependencies = models.JSONField(null=True)
89
93
  description = models.TextField(null=True)
@@ -100,7 +104,7 @@ class Package(models.Model):
100
104
  nuget_id = models.CharField(max_length=128)
101
105
  project_url = models.URLField()
102
106
  published = models.DateTimeField(auto_now_add=True, blank=True)
103
- references = models.JSONField(default=dict) # type: ignore[var-annotated]
107
+ references = models.JSONField(default=dict)
104
108
  release_notes = models.TextField(null=True)
105
109
  require_license_acceptance = models.BooleanField(default=False)
106
110
  size = models.PositiveIntegerField()
@@ -116,5 +120,11 @@ class Package(models.Model):
116
120
  version3 = models.PositiveIntegerField(null=True)
117
121
  version_beta = models.CharField(max_length=128, null=True)
118
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
119
129
  def __str__(self) -> str:
120
130
  return f'{self.title} {self.version}'