Blacked the code
This commit is contained in:
parent
b6fc0051fa
commit
62ca3bd736
279 changed files with 8890 additions and 9556 deletions
|
|
@ -16,20 +16,19 @@ class TokenHeaderAuth(BaseJSONWebTokenAuthentication):
|
|||
def get_jwt_value(self, request):
|
||||
|
||||
try:
|
||||
qs = request.get('query_string', b'').decode('utf-8')
|
||||
qs = request.get("query_string", b"").decode("utf-8")
|
||||
parsed = parse_qs(qs)
|
||||
token = parsed['token'][0]
|
||||
token = parsed["token"][0]
|
||||
except KeyError:
|
||||
raise exceptions.AuthenticationFailed('No token')
|
||||
raise exceptions.AuthenticationFailed("No token")
|
||||
|
||||
if not token:
|
||||
raise exceptions.AuthenticationFailed('Empty token')
|
||||
raise exceptions.AuthenticationFailed("Empty token")
|
||||
|
||||
return token
|
||||
|
||||
|
||||
class TokenAuthMiddleware:
|
||||
|
||||
def __init__(self, inner):
|
||||
# Store the ASGI application we were passed
|
||||
self.inner = inner
|
||||
|
|
@ -41,5 +40,5 @@ class TokenAuthMiddleware:
|
|||
except (User.DoesNotExist, exceptions.AuthenticationFailed):
|
||||
user = AnonymousUser()
|
||||
|
||||
scope['user'] = user
|
||||
scope["user"] = user
|
||||
return self.inner(scope)
|
||||
|
|
|
|||
|
|
@ -6,34 +6,34 @@ from rest_framework_jwt import authentication
|
|||
from rest_framework_jwt.settings import api_settings
|
||||
|
||||
|
||||
class JSONWebTokenAuthenticationQS(
|
||||
authentication.BaseJSONWebTokenAuthentication):
|
||||
class JSONWebTokenAuthenticationQS(authentication.BaseJSONWebTokenAuthentication):
|
||||
|
||||
www_authenticate_realm = 'api'
|
||||
www_authenticate_realm = "api"
|
||||
|
||||
def get_jwt_value(self, request):
|
||||
token = request.query_params.get('jwt')
|
||||
if 'jwt' in request.query_params and not token:
|
||||
msg = _('Invalid Authorization header. No credentials provided.')
|
||||
token = request.query_params.get("jwt")
|
||||
if "jwt" in request.query_params and not token:
|
||||
msg = _("Invalid Authorization header. No credentials provided.")
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
return token
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return '{0} realm="{1}"'.format(
|
||||
api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
|
||||
api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm
|
||||
)
|
||||
|
||||
|
||||
class BearerTokenHeaderAuth(
|
||||
authentication.BaseJSONWebTokenAuthentication):
|
||||
class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication):
|
||||
"""
|
||||
For backward compatibility purpose, we used Authorization: JWT <token>
|
||||
but Authorization: Bearer <token> is probably better.
|
||||
"""
|
||||
www_authenticate_realm = 'api'
|
||||
|
||||
www_authenticate_realm = "api"
|
||||
|
||||
def get_jwt_value(self, request):
|
||||
auth = authentication.get_authorization_header(request).split()
|
||||
auth_header_prefix = 'bearer'
|
||||
auth_header_prefix = "bearer"
|
||||
|
||||
if not auth:
|
||||
if api_settings.JWT_AUTH_COOKIE:
|
||||
|
|
@ -44,14 +44,16 @@ class BearerTokenHeaderAuth(
|
|||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
msg = _('Invalid Authorization header. No credentials provided.')
|
||||
msg = _("Invalid Authorization header. No credentials provided.")
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
elif len(auth) > 2:
|
||||
msg = _('Invalid Authorization header. Credentials string '
|
||||
'should not contain spaces.')
|
||||
msg = _(
|
||||
"Invalid Authorization header. Credentials string "
|
||||
"should not contain spaces."
|
||||
)
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
return auth[1]
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return '{0} realm="{1}"'.format('Bearer', self.www_authenticate_realm)
|
||||
return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from funkwhale_api.common import channels
|
|||
class JsonAuthConsumer(JsonWebsocketConsumer):
|
||||
def connect(self):
|
||||
try:
|
||||
assert self.scope['user'].pk is not None
|
||||
assert self.scope["user"].pk is not None
|
||||
except (AssertionError, AttributeError, KeyError):
|
||||
return self.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,19 @@ from dynamic_preferences.registries import global_preferences_registry
|
|||
|
||||
from funkwhale_api.common import preferences
|
||||
|
||||
common = types.Section('common')
|
||||
common = types.Section("common")
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class APIAutenticationRequired(
|
||||
preferences.DefaultFromSettingMixin, types.BooleanPreference):
|
||||
preferences.DefaultFromSettingMixin, types.BooleanPreference
|
||||
):
|
||||
section = common
|
||||
name = 'api_authentication_required'
|
||||
verbose_name = 'API Requires authentication'
|
||||
setting = 'API_AUTHENTICATION_REQUIRED'
|
||||
name = "api_authentication_required"
|
||||
verbose_name = "API Requires authentication"
|
||||
setting = "API_AUTHENTICATION_REQUIRED"
|
||||
help_text = (
|
||||
'If disabled, anonymous users will be able to query the API'
|
||||
'and access music data (as well as other data exposed in the API '
|
||||
'without specific permissions).'
|
||||
"If disabled, anonymous users will be able to query the API"
|
||||
"and access music data (as well as other data exposed in the API "
|
||||
"without specific permissions)."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,34 +6,31 @@ from funkwhale_api.music import utils
|
|||
|
||||
|
||||
PRIVACY_LEVEL_CHOICES = [
|
||||
('me', 'Only me'),
|
||||
('followers', 'Me and my followers'),
|
||||
('instance', 'Everyone on my instance, and my followers'),
|
||||
('everyone', 'Everyone, including people on other instances'),
|
||||
("me", "Only me"),
|
||||
("followers", "Me and my followers"),
|
||||
("instance", "Everyone on my instance, and my followers"),
|
||||
("everyone", "Everyone, including people on other instances"),
|
||||
]
|
||||
|
||||
|
||||
def get_privacy_field():
|
||||
return models.CharField(
|
||||
max_length=30, choices=PRIVACY_LEVEL_CHOICES, default='instance')
|
||||
max_length=30, choices=PRIVACY_LEVEL_CHOICES, default="instance"
|
||||
)
|
||||
|
||||
|
||||
def privacy_level_query(user, lookup_field='privacy_level'):
|
||||
def privacy_level_query(user, lookup_field="privacy_level"):
|
||||
if user.is_anonymous:
|
||||
return models.Q(**{
|
||||
lookup_field: 'everyone',
|
||||
})
|
||||
return models.Q(**{lookup_field: "everyone"})
|
||||
|
||||
return models.Q(**{
|
||||
'{}__in'.format(lookup_field): [
|
||||
'followers', 'instance', 'everyone'
|
||||
]
|
||||
})
|
||||
return models.Q(
|
||||
**{"{}__in".format(lookup_field): ["followers", "instance", "everyone"]}
|
||||
)
|
||||
|
||||
|
||||
class SearchFilter(django_filters.CharFilter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.search_fields = kwargs.pop('search_fields')
|
||||
self.search_fields = kwargs.pop("search_fields")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
|
|
|
|||
|
|
@ -4,17 +4,20 @@ from funkwhale_api.common import scripts
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Run a specific script from funkwhale_api/common/scripts/'
|
||||
help = "Run a specific script from funkwhale_api/common/scripts/"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('script_name', nargs='?', type=str)
|
||||
parser.add_argument("script_name", nargs="?", type=str)
|
||||
parser.add_argument(
|
||||
'--noinput', '--no-input', action='store_false', dest='interactive',
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help="Do NOT prompt the user for input of any kind.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
name = options['script_name']
|
||||
name = options["script_name"]
|
||||
if not name:
|
||||
self.show_help()
|
||||
|
||||
|
|
@ -23,44 +26,44 @@ class Command(BaseCommand):
|
|||
script = available_scripts[name]
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
'{} is not a valid script. Run python manage.py script for a '
|
||||
'list of available scripts'.format(name))
|
||||
"{} is not a valid script. Run python manage.py script for a "
|
||||
"list of available scripts".format(name)
|
||||
)
|
||||
|
||||
self.stdout.write('')
|
||||
if options['interactive']:
|
||||
self.stdout.write("")
|
||||
if options["interactive"]:
|
||||
message = (
|
||||
'Are you sure you want to execute the script {}?\n\n'
|
||||
"Are you sure you want to execute the script {}?\n\n"
|
||||
"Type 'yes' to continue, or 'no' to cancel: "
|
||||
).format(name)
|
||||
if input(''.join(message)) != 'yes':
|
||||
if input("".join(message)) != "yes":
|
||||
raise CommandError("Script cancelled.")
|
||||
script['entrypoint'](self, **options)
|
||||
script["entrypoint"](self, **options)
|
||||
|
||||
def show_help(self):
|
||||
indentation = 4
|
||||
self.stdout.write('')
|
||||
self.stdout.write('Available scripts:')
|
||||
self.stdout.write('Launch with: python manage.py <script_name>')
|
||||
self.stdout.write("")
|
||||
self.stdout.write("Available scripts:")
|
||||
self.stdout.write("Launch with: python manage.py <script_name>")
|
||||
available_scripts = self.get_scripts()
|
||||
for name, script in sorted(available_scripts.items()):
|
||||
self.stdout.write('')
|
||||
self.stdout.write("")
|
||||
self.stdout.write(self.style.SUCCESS(name))
|
||||
self.stdout.write('')
|
||||
for line in script['help'].splitlines():
|
||||
self.stdout.write(' {}'.format(line))
|
||||
self.stdout.write('')
|
||||
self.stdout.write("")
|
||||
for line in script["help"].splitlines():
|
||||
self.stdout.write(" {}".format(line))
|
||||
self.stdout.write("")
|
||||
|
||||
def get_scripts(self):
|
||||
available_scripts = [
|
||||
k for k in sorted(scripts.__dict__.keys())
|
||||
if not k.startswith('__')
|
||||
k for k in sorted(scripts.__dict__.keys()) if not k.startswith("__")
|
||||
]
|
||||
data = {}
|
||||
for name in available_scripts:
|
||||
module = getattr(scripts, name)
|
||||
data[name] = {
|
||||
'name': name,
|
||||
'help': module.__doc__.strip(),
|
||||
'entrypoint': module.main
|
||||
"name": name,
|
||||
"help": module.__doc__.strip(),
|
||||
"entrypoint": module.main,
|
||||
}
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -7,6 +7,4 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
UnaccentExtension()
|
||||
]
|
||||
operations = [UnaccentExtension()]
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ from rest_framework.pagination import PageNumberPagination
|
|||
|
||||
|
||||
class FunkwhalePagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 50
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ from funkwhale_api.common import preferences
|
|||
|
||||
|
||||
class ConditionalAuthentication(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if preferences.get('common__api_authentication_required'):
|
||||
if preferences.get("common__api_authentication_required"):
|
||||
return request.user and request.user.is_authenticated
|
||||
return True
|
||||
|
||||
|
|
@ -28,24 +27,25 @@ class OwnerPermission(BasePermission):
|
|||
owner_field = 'owner'
|
||||
owner_checks = ['read', 'write']
|
||||
"""
|
||||
|
||||
perms_map = {
|
||||
'GET': 'read',
|
||||
'OPTIONS': 'read',
|
||||
'HEAD': 'read',
|
||||
'POST': 'write',
|
||||
'PUT': 'write',
|
||||
'PATCH': 'write',
|
||||
'DELETE': 'write',
|
||||
"GET": "read",
|
||||
"OPTIONS": "read",
|
||||
"HEAD": "read",
|
||||
"POST": "write",
|
||||
"PUT": "write",
|
||||
"PATCH": "write",
|
||||
"DELETE": "write",
|
||||
}
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
method_check = self.perms_map[request.method]
|
||||
owner_checks = getattr(view, 'owner_checks', ['read', 'write'])
|
||||
owner_checks = getattr(view, "owner_checks", ["read", "write"])
|
||||
if method_check not in owner_checks:
|
||||
# check not enabled
|
||||
return True
|
||||
|
||||
owner_field = getattr(view, 'owner_field', 'user')
|
||||
owner_field = getattr(view, "owner_field", "user")
|
||||
owner = operator.attrgetter(owner_field)(obj)
|
||||
if owner != request.user:
|
||||
raise Http404
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def get(pref):
|
|||
|
||||
|
||||
class StringListSerializer(serializers.BaseSerializer):
|
||||
separator = ','
|
||||
separator = ","
|
||||
sort = True
|
||||
|
||||
@classmethod
|
||||
|
|
@ -27,8 +27,8 @@ class StringListSerializer(serializers.BaseSerializer):
|
|||
|
||||
if type(value) not in [list, tuple]:
|
||||
raise cls.exception(
|
||||
"Cannot serialize, value {} is not a list or a tuple".format(
|
||||
value))
|
||||
"Cannot serialize, value {} is not a list or a tuple".format(value)
|
||||
)
|
||||
|
||||
if cls.sort:
|
||||
value = sorted(value)
|
||||
|
|
@ -38,7 +38,7 @@ class StringListSerializer(serializers.BaseSerializer):
|
|||
def to_python(cls, value, **kwargs):
|
||||
if not value:
|
||||
return []
|
||||
return value.split(',')
|
||||
return value.split(",")
|
||||
|
||||
|
||||
class StringListPreference(types.BasePreferenceType):
|
||||
|
|
@ -47,5 +47,5 @@ class StringListPreference(types.BasePreferenceType):
|
|||
|
||||
def get_api_additional_data(self):
|
||||
d = super(StringListPreference, self).get_api_additional_data()
|
||||
d['choices'] = self.get('choices')
|
||||
d["choices"] = self.get("choices")
|
||||
return d
|
||||
|
|
|
|||
|
|
@ -8,22 +8,22 @@ from funkwhale_api.users import models
|
|||
from django.contrib.auth.models import Permission
|
||||
|
||||
mapping = {
|
||||
'dynamic_preferences.change_globalpreferencemodel': 'settings',
|
||||
'music.add_importbatch': 'library',
|
||||
'federation.change_library': 'federation',
|
||||
"dynamic_preferences.change_globalpreferencemodel": "settings",
|
||||
"music.add_importbatch": "library",
|
||||
"federation.change_library": "federation",
|
||||
}
|
||||
|
||||
|
||||
def main(command, **kwargs):
|
||||
for codename, user_permission in sorted(mapping.items()):
|
||||
app_label, c = codename.split('.')
|
||||
p = Permission.objects.get(
|
||||
content_type__app_label=app_label, codename=c)
|
||||
app_label, c = codename.split(".")
|
||||
p = Permission.objects.get(content_type__app_label=app_label, codename=c)
|
||||
users = models.User.objects.filter(
|
||||
Q(groups__permissions=p) | Q(user_permissions=p)).distinct()
|
||||
Q(groups__permissions=p) | Q(user_permissions=p)
|
||||
).distinct()
|
||||
total = users.count()
|
||||
|
||||
command.stdout.write('Updating {} users with {} permission...'.format(
|
||||
total, user_permission
|
||||
))
|
||||
users.update(**{'permission_{}'.format(user_permission): True})
|
||||
command.stdout.write(
|
||||
"Updating {} users with {} permission...".format(total, user_permission)
|
||||
)
|
||||
users.update(**{"permission_{}".format(user_permission): True})
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ You can launch it just to check how it works.
|
|||
|
||||
|
||||
def main(command, **kwargs):
|
||||
command.stdout.write('Test script run successfully')
|
||||
command.stdout.write("Test script run successfully")
|
||||
|
|
|
|||
|
|
@ -17,67 +17,68 @@ class ActionSerializer(serializers.Serializer):
|
|||
dangerous_actions = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset')
|
||||
self.queryset = kwargs.pop("queryset")
|
||||
if self.actions is None:
|
||||
raise ValueError(
|
||||
'You must declare a list of actions on '
|
||||
'the serializer class')
|
||||
"You must declare a list of actions on " "the serializer class"
|
||||
)
|
||||
|
||||
for action in self.actions:
|
||||
handler_name = 'handle_{}'.format(action)
|
||||
assert hasattr(self, handler_name), (
|
||||
'{} miss a {} method'.format(
|
||||
self.__class__.__name__, handler_name)
|
||||
handler_name = "handle_{}".format(action)
|
||||
assert hasattr(self, handler_name), "{} miss a {} method".format(
|
||||
self.__class__.__name__, handler_name
|
||||
)
|
||||
super().__init__(self, *args, **kwargs)
|
||||
|
||||
def validate_action(self, value):
|
||||
if value not in self.actions:
|
||||
raise serializers.ValidationError(
|
||||
'{} is not a valid action. Pick one of {}.'.format(
|
||||
value, ', '.join(self.actions)
|
||||
"{} is not a valid action. Pick one of {}.".format(
|
||||
value, ", ".join(self.actions)
|
||||
)
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_objects(self, value):
|
||||
qs = None
|
||||
if value == 'all':
|
||||
return self.queryset.all().order_by('id')
|
||||
if value == "all":
|
||||
return self.queryset.all().order_by("id")
|
||||
if type(value) in [list, tuple]:
|
||||
return self.queryset.filter(pk__in=value).order_by('id')
|
||||
return self.queryset.filter(pk__in=value).order_by("id")
|
||||
|
||||
raise serializers.ValidationError(
|
||||
'{} is not a valid value for objects. You must provide either a '
|
||||
'list of identifiers or the string "all".'.format(value))
|
||||
"{} is not a valid value for objects. You must provide either a "
|
||||
'list of identifiers or the string "all".'.format(value)
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
dangerous = data['action'] in self.dangerous_actions
|
||||
if dangerous and self.initial_data['objects'] == 'all':
|
||||
dangerous = data["action"] in self.dangerous_actions
|
||||
if dangerous and self.initial_data["objects"] == "all":
|
||||
raise serializers.ValidationError(
|
||||
'This action is to dangerous to be applied to all objects')
|
||||
if self.filterset_class and 'filters' in data:
|
||||
"This action is to dangerous to be applied to all objects"
|
||||
)
|
||||
if self.filterset_class and "filters" in data:
|
||||
qs_filterset = self.filterset_class(
|
||||
data['filters'], queryset=data['objects'])
|
||||
data["filters"], queryset=data["objects"]
|
||||
)
|
||||
try:
|
||||
assert qs_filterset.form.is_valid()
|
||||
except (AssertionError, TypeError):
|
||||
raise serializers.ValidationError('Invalid filters')
|
||||
data['objects'] = qs_filterset.qs
|
||||
raise serializers.ValidationError("Invalid filters")
|
||||
data["objects"] = qs_filterset.qs
|
||||
|
||||
data['count'] = data['objects'].count()
|
||||
if data['count'] < 1:
|
||||
raise serializers.ValidationError(
|
||||
'No object matching your request')
|
||||
data["count"] = data["objects"].count()
|
||||
if data["count"] < 1:
|
||||
raise serializers.ValidationError("No object matching your request")
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
handler_name = 'handle_{}'.format(self.validated_data['action'])
|
||||
handler_name = "handle_{}".format(self.validated_data["action"])
|
||||
handler = getattr(self, handler_name)
|
||||
result = handler(self.validated_data['objects'])
|
||||
result = handler(self.validated_data["objects"])
|
||||
payload = {
|
||||
'updated': self.validated_data['count'],
|
||||
'action': self.validated_data['action'],
|
||||
'result': result,
|
||||
"updated": self.validated_data["count"],
|
||||
"action": self.validated_data["action"],
|
||||
"result": result,
|
||||
}
|
||||
return payload
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import funkwhale_api
|
|||
|
||||
|
||||
def get_user_agent():
|
||||
return 'python-requests (funkwhale/{}; +{})'.format(
|
||||
funkwhale_api.__version__,
|
||||
settings.FUNKWHALE_URL
|
||||
return "python-requests (funkwhale/{}; +{})".format(
|
||||
funkwhale_api.__version__, settings.FUNKWHALE_URL
|
||||
)
|
||||
|
||||
|
||||
def get_session():
|
||||
s = requests.Session()
|
||||
s.headers['User-Agent'] = get_user_agent()
|
||||
s.headers["User-Agent"] = get_user_agent()
|
||||
return s
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class ASCIIFileSystemStorage(FileSystemStorage):
|
|||
"""
|
||||
Convert unicode characters in name to ASCII characters.
|
||||
"""
|
||||
|
||||
def get_valid_name(self, name):
|
||||
name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore')
|
||||
name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore")
|
||||
return super().get_valid_name(name)
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
|
|||
field = getattr(instance, field_name)
|
||||
current_name, extension = os.path.splitext(field.name)
|
||||
|
||||
new_name_with_extension = '{}{}'.format(new_name, extension)
|
||||
new_name_with_extension = "{}{}".format(new_name, extension)
|
||||
try:
|
||||
shutil.move(field.path, new_name_with_extension)
|
||||
except FileNotFoundError:
|
||||
if not allow_missing_file:
|
||||
raise
|
||||
print('Skipped missing file', field.path)
|
||||
print("Skipped missing file", field.path)
|
||||
initial_path = os.path.dirname(field.name)
|
||||
field.name = os.path.join(initial_path, new_name_with_extension)
|
||||
instance.save()
|
||||
|
|
@ -23,9 +23,7 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False):
|
|||
|
||||
|
||||
def on_commit(f, *args, **kwargs):
|
||||
return transaction.on_commit(
|
||||
lambda: f(*args, **kwargs)
|
||||
)
|
||||
return transaction.on_commit(lambda: f(*args, **kwargs))
|
||||
|
||||
|
||||
def set_query_parameter(url, **kwargs):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue