diff --git a/order/managers.py b/order/managers.py index 110bb58..ac8d4a0 100644 --- a/order/managers.py +++ b/order/managers.py @@ -17,3 +17,14 @@ def get_orders_by_reformer(self, user: User = None): .select_related("service", "service__market", "service__market__reformer") .filter(service__market__reformer__user=user) ) + + +class OrderStatusManager(models.Manager): + + def get_order_status_by_order_uuid(self, order_uuid: str): + return ( + super() + .get_queryset() + .select_related("order") + .filter(order__order_uuid=order_uuid) + ) diff --git a/order/mixins.py b/order/mixins.py index 8eeba8a..289bf5c 100644 --- a/order/mixins.py +++ b/order/mixins.py @@ -1,10 +1,29 @@ -from datetime import date, timedelta -from typing import Any, override +from datetime import date +from typing import Any, Optional, override from django.db.models import QuerySet from rest_framework.exceptions import ValidationError from core.mixins import QueryParamMixin +from order.models import _OrderStatus + + +class OrderStatusQueryParamMixin(QueryParamMixin): + + def __init__(self): + super().__init__() + self.ALLOWED_FILTER_ARRAY = [choice[0] for choice in _OrderStatus.choices] + + @override + def apply_filters_and_sorting( + self, queryset: QuerySet, status: Optional[str] + ) -> QuerySet: + if status: + if status not in self.ALLOWED_FILTER_ARRAY: + raise ValidationError("Invalid status query parameter") + return queryset.filter(status=status) + else: + return queryset class OrderQueryParamMinxin(QueryParamMixin): diff --git a/order/models.py b/order/models.py index 05ec5a7..914c886 100644 --- a/order/models.py +++ b/order/models.py @@ -3,7 +3,7 @@ from django.db import models from core.models import TimeStampedModel -from order.managers import OrderManager +from order.managers import OrderManager, OrderStatusManager def get_order_image_upload_path(instance, filename): @@ -96,6 +96,8 @@ class OrderStatus(TimeStampedModel): default="pending", ) + objects = OrderStatusManager() + class Meta: db_table = "order_status" diff --git a/order/serializers/order_retrieve_serializer.py b/order/serializers/order_retrieve_serializer.py index fadf832..f2a6f97 100644 --- a/order/serializers/order_retrieve_serializer.py +++ b/order/serializers/order_retrieve_serializer.py @@ -12,14 +12,11 @@ from order.models import Order from order.serializers.delivery_status_serializer import DeliveryStatusSerializer from order.serializers.order_create_serializer import OrderImageSerializer -from order.serializers.order_status_serializer import OrderStatusSerailzier +from order.serializers.order_status_serializer import OrderStatusRetrieveSerializer from order.serializers.orderer_information_serializer import ( OrdererInformationSerializer, ) from order.serializers.transaction_serializer import TransactionSerializer -from users.serializers.reformer_serializer.reformer_profile_serializer import ( - ReformerProfileSerializer, -) from users.serializers.user_serializer.user_information_serializer import ( UserOrderInformationSerializer, ) @@ -29,7 +26,7 @@ class OrderRetrieveSerializer(serializers.ModelSerializer): service_info = serializers.SerializerMethodField(read_only=True) materials = ServiceMaterialRetrieveSerializer(many=True, read_only=True) additional_options = ServiceOptionRetrieveSerializer(many=True, read_only=True) - order_status = OrderStatusSerailzier(many=True, read_only=True) + order_status = OrderStatusRetrieveSerializer(many=True, read_only=True) orderer_information = serializers.SerializerMethodField(read_only=True) transaction = TransactionSerializer(read_only=True) delivery_status = serializers.SerializerMethodField(read_only=True) diff --git a/order/serializers/order_status_serializer.py b/order/serializers/order_status_serializer.py index e6ae0be..8f5aefc 100644 --- a/order/serializers/order_status_serializer.py +++ b/order/serializers/order_status_serializer.py @@ -3,7 +3,19 @@ from order.models import OrderStatus -class OrderStatusSerailzier(serializers.ModelSerializer): +class OrderStatusRetrieveSerializer(serializers.ModelSerializer): + class Meta: model = OrderStatus fields = ["status", "created"] + + +class OrderStatusRejectedSerailzier(serializers.ModelSerializer): + rejected_reason = serializers.SerializerMethodField(read_only=True) + + def get_rejected_reason(self, obj): + return obj.order.rejected_reason + + class Meta: + model = OrderStatus + fields = ["status", "rejected_reason", "created"] diff --git a/order/tests.py b/order/tests.py index dbcea73..7566998 100644 --- a/order/tests.py +++ b/order/tests.py @@ -796,6 +796,68 @@ def test_update_order_status_invalid_status(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_get_rejceted_order_status_with_order_uuid(self): + # Given + # 주문 1개 생성 + self.generate_order(num=1) + self.assertEqual(Order.objects.all().count(), 1) + order = Order.objects.all().first() + + # update order status update to reject + response = self.reformer_client.patch( + path=f"/api/orders/{str(order.order_uuid)}/status", + data={"status": "rejected", "rejected_reason": "this is test"}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + order.refresh_from_db() + self.assertEqual(order.order_status.first().status, "rejected") + self.assertEqual(order.rejected_reason, "this is test") + + # When + response = self.user_client.get( + path=f"/api/orders/{str(order.order_uuid)}/status", + query_params={"filter": "rejected"}, + format="json", + ) + + # Then + order_status: OrderStatus = order.order_status.first() + self.assertEqual(order_status.status, response.data.get("status")) + self.assertEqual(order.rejected_reason, response.data.get("rejected_reason")) + self.assertIn("created", response.data) + + def test_get_order_status_with_order_uuid(self): + # Given + # 주문 1개 생성 + self.generate_order(num=1) + self.assertEqual(Order.objects.all().count(), 1) + order = Order.objects.all().first() + + # update order status update to received + response = self.reformer_client.patch( + path=f"/api/orders/{str(order.order_uuid)}/status", + data={"status": "received"}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + order.refresh_from_db() + self.assertEqual(order.order_status.first().status, "received") + + # When + response = self.user_client.get( + path=f"/api/orders/{str(order.order_uuid)}/status", + query_params={"filter": "received"}, + format="json", + ) + + # Then + order_status: OrderStatus = order.order_status.first() + self.assertEqual(order_status.status, response.data.get("status")) + self.assertIn("created", response.data) + def tearDown(self): patch.stopall() # 활성화된 Mocking 중단 Order.objects.all().delete() diff --git a/order/urls.py b/order/urls.py index 6473fc3..8ae9d18 100644 --- a/order/urls.py +++ b/order/urls.py @@ -11,7 +11,7 @@ ), path( "//status", - OrderStatusUpdateView.as_view(), + OrderStatusView.as_view(), name="order_update", ), path( diff --git a/order/views/order_view.py b/order/views/order_view.py index d960548..a17a92b 100644 --- a/order/views/order_view.py +++ b/order/views/order_view.py @@ -1,4 +1,5 @@ import logging +from typing import Optional from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet @@ -10,7 +11,7 @@ from core.exceptions import view_exception_handler from core.permissions import IsReformer from market.models import Service -from order.mixins import OrderQueryParamMinxin +from order.mixins import OrderQueryParamMinxin, OrderStatusQueryParamMixin from order.models import DeliveryInformation, Order, OrderStatus, _OrderStatus from order.pagination import OrderListPagination from order.serializers.delivery_status_serializer import DeliveryStatusSerializer @@ -19,6 +20,10 @@ OrderCreateSerializer, ) from order.serializers.order_retrieve_serializer import OrderRetrieveSerializer +from order.serializers.order_status_serializer import ( + OrderStatusRejectedSerailzier, + OrderStatusRetrieveSerializer, +) logger = logging.getLogger(__name__) @@ -67,14 +72,10 @@ def get(self, request): @view_exception_handler def post(self, request): - logger.debug("POST : /api/orders") - logger.debug(request.data) - serializer: OrderCreateSerializer = OrderCreateSerializer( data=request.data, context={"request": request} ) serializer.is_valid(raise_exception=True) - logger.debug("serializer 검증 완료 -> save() 호출") order: Order = serializer.save() # create 호출 response_serializer: OrderCreateResponseSerializer = ( @@ -104,7 +105,7 @@ def get(self, request, **kwargs): return Response(data=serializer.data, status=status.HTTP_200_OK) -class OrderStatusUpdateView(APIView): +class OrderStatusView(OrderStatusQueryParamMixin, APIView): """ 주문 UUID를 사용하여 주문 상태 정보를 업데이트 하는 API 구현체 """ @@ -112,12 +113,40 @@ class OrderStatusUpdateView(APIView): permission_classes = [IsAuthenticated] @view_exception_handler - def patch(self, request, **kwargs): + def get(self, request, **kwargs) -> Response: + order_uuid: Optional[str] = kwargs.get("order_uuid", None) + if not order_uuid: + raise ValueError("order_uuid path parameter is required") + + _status: Optional[str] = request.GET.get("filter", None) + queryset: QuerySet = OrderStatus.objects.get_order_status_by_order_uuid( + order_uuid=order_uuid + ) + order_status: OrderStatus = self.apply_filters_and_sorting( + queryset=queryset, status=_status + ).first() + + if _status == _OrderStatus.REJECTED: + serializer: OrderStatusRejectedSerailzier = OrderStatusRejectedSerailzier( + instance=order_status + ) + else: + serializer: OrderStatusRetrieveSerializer = OrderStatusRetrieveSerializer( + instance=order_status + ) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + @view_exception_handler + def patch(self, request, **kwargs) -> Response: + order_uuid: Optional[str] = kwargs.get("order_uuid", None) + if not order_uuid: + raise ValueError("order_uuid path parameter is required") + _status: str = request.data.get("status") if _status is None: raise ValueError("status query parameter is required") - order: Order = Order.objects.filter(order_uuid=kwargs.get("order_uuid")).first() + order: Order = Order.objects.filter(order_uuid=order_uuid).first() if not order: raise ObjectDoesNotExist("order not found") order_status: OrderStatus = OrderStatus.objects.filter(order=order).first() @@ -138,6 +167,7 @@ def patch(self, request, **kwargs): order_status.status = _OrderStatus.END case _: raise ValueError("invalid status query parameter") + order.save() order_status.save() return Response(status=status.HTTP_200_OK) diff --git a/poetry.lock b/poetry.lock index d509ac3..d779045 100644 --- a/poetry.lock +++ b/poetry.lock @@ -60,17 +60,17 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.36.16" +version = "1.37.16" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.36.16-py3-none-any.whl", hash = "sha256:b10583bf8bd35be1b4027ee7e26b7cdf2078c79eab18357fd602cecb6d39400b"}, - {file = "boto3-1.36.16.tar.gz", hash = "sha256:0cf92ca0538ab115447e1c58050d43e1273e88c58ddfea2b6f133fdc508b400a"}, + {file = "boto3-1.37.16-py3-none-any.whl", hash = "sha256:7ba243b8f9e11ea8856b1875293f1d43f3aa04960d823f2016cb624f47d45048"}, + {file = "boto3-1.37.16.tar.gz", hash = "sha256:c9e820b5c7363329951ca3429fa5d51137cbd9766e063d15f212ee407fff89e9"}, ] [package.dependencies] -botocore = ">=1.36.16,<1.37.0" +botocore = ">=1.37.16,<1.38.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.11.0,<0.12.0" @@ -79,13 +79,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.36.16" +version = "1.37.16" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.36.16-py3-none-any.whl", hash = "sha256:aca0348ccd730332082489b6817fdf89e1526049adcf6e9c8c11c96dd9f42c03"}, - {file = "botocore-1.36.16.tar.gz", hash = "sha256:10c6aa386ba1a9a0faef6bb5dbfc58fc2563a3c6b95352e86a583cd5f14b11f3"}, + {file = "botocore-1.37.16-py3-none-any.whl", hash = "sha256:d74d04830ead12933a96dc407175ae98b32a5dd0059d7d2b28fc7aa4ed9d3b48"}, + {file = "botocore-1.37.16.tar.gz", hash = "sha256:26bdf95d5448682bdb05ff4b15b007f977d027c0d791482b43e69f098e609978"}, ] [package.dependencies] @@ -123,13 +123,13 @@ files = [ [[package]] name = "django" -version = "5.1.6" +version = "5.1.7" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.1.6-py3-none-any.whl", hash = "sha256:8d203400bc2952fbfb287c2bbda630297d654920c72a73cc82a9ad7926feaad5"}, - {file = "Django-5.1.6.tar.gz", hash = "sha256:1e39eafdd1b185e761d9fab7a9f0b9fa00af1b37b25ad980a8aa0dac13535690"}, + {file = "Django-5.1.7-py3-none-any.whl", hash = "sha256:1323617cb624add820cb9611cdcc788312d250824f92ca6048fda8625514af2b"}, + {file = "Django-5.1.7.tar.gz", hash = "sha256:30de4ee43a98e5d3da36a9002f287ff400b43ca51791920bfb35f6917bfe041c"}, ] [package.dependencies] @@ -172,13 +172,13 @@ Django = ">=3.2" [[package]] name = "django-storages" -version = "1.14.4" +version = "1.14.5" description = "Support for many storage backends in Django" optional = false python-versions = ">=3.7" files = [ - {file = "django-storages-1.14.4.tar.gz", hash = "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f"}, - {file = "django_storages-1.14.4-py3-none-any.whl", hash = "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3"}, + {file = "django_storages-1.14.5-py3-none-any.whl", hash = "sha256:5ce9c69426f24f379821fd688442314e4aa03de87ae43183c4e16915f4c165d4"}, + {file = "django_storages-1.14.5.tar.gz", hash = "sha256:ace80dbee311258453e30cd5cfd91096b834180ccf09bc1f4d2cb6d38d68571a"}, ] [package.dependencies] @@ -189,7 +189,7 @@ Django = ">=3.2" azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"] boto3 = ["boto3 (>=1.4.4)"] dropbox = ["dropbox (>=7.2.1)"] -google = ["google-cloud-storage (>=1.27)"] +google = ["google-cloud-storage (>=1.32)"] libcloud = ["apache-libcloud"] s3 = ["boto3 (>=1.4.4)"] sftp = ["paramiko (>=1.15)"] @@ -210,25 +210,25 @@ django = ">=4.2" [[package]] name = "djangorestframework-simplejwt" -version = "5.4.0" +version = "5.5.0" description = "A minimal JSON Web Token authentication plugin for Django REST Framework" optional = false python-versions = ">=3.9" files = [ - {file = "djangorestframework_simplejwt-5.4.0-py3-none-any.whl", hash = "sha256:7aec953db9ed4163430c16d086eecb0f028f814ce6bba62b06c25919261e9077"}, - {file = "djangorestframework_simplejwt-5.4.0.tar.gz", hash = "sha256:cccecce1a0e1a4a240fae80da73e5fc23055bababb8b67de88fa47cd36822320"}, + {file = "djangorestframework_simplejwt-5.5.0-py3-none-any.whl", hash = "sha256:4ef6b38af20cdde4a4a51d1fd8e063cbbabb7b45f149cc885d38d905c5a62edb"}, + {file = "djangorestframework_simplejwt-5.5.0.tar.gz", hash = "sha256:474a1b737067e6462b3609627a392d13a4da8a08b1f0574104ac6d7b1406f90e"}, ] [package.dependencies] django = ">=4.2" djangorestframework = ">=3.14" -pyjwt = ">=1.7.1,<3" +pyjwt = ">=1.7.1,<2.10.0" [package.extras] crypto = ["cryptography (>=3.3.1)"] -dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "flake8", "freezegun", "ipython", "isort", "pep8", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel"] +dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "freezegun", "ipython", "pre-commit", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "pyupgrade", "ruff", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel", "yesqa"] doc = ["Sphinx (>=1.6.5,<2)", "sphinx_rtd_theme (>=0.1.9)"] -lint = ["flake8", "isort", "pep8"] +lint = ["pre-commit", "pyupgrade", "ruff", "yesqa"] python-jose = ["python-jose (==3.3.0)"] test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] @@ -249,13 +249,13 @@ typing-extensions = "*" [[package]] name = "flake8" -version = "7.1.1" +version = "7.1.2" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" files = [ - {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, - {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, + {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, + {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, ] [package.dependencies] @@ -515,19 +515,19 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "psycopg2-binary" @@ -583,6 +583,7 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -629,13 +630,13 @@ files = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, ] [package.extras] @@ -674,20 +675,20 @@ cli = ["click (>=5.0)"] [[package]] name = "s3transfer" -version = "0.11.2" +version = "0.11.4" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.8" files = [ - {file = "s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"}, - {file = "s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f"}, + {file = "s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d"}, + {file = "s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679"}, ] [package.dependencies] -botocore = ">=1.36.0,<2.0a.0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "six"