diff --git a/Pipfile b/Pipfile index f18f8132..7ed818f5 100644 --- a/Pipfile +++ b/Pipfile @@ -38,6 +38,7 @@ uritemplate = "*" tqdm = "*" lxml = "*" django-ses = "*" +pydantic = "*" [dev-packages] black = "*" diff --git a/Pipfile.lock b/Pipfile.lock index cec90b3c..a3f652c5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b622760b4f6c86bfd3aa31fc9277594e08e7175331bd1e4121551e9e80ec8cef" + "sha256": "f3ada264b44d60d21bc04efaa5502e2097d06c9d28b0079fbdae721f944d6263" }, "pipfile-spec": 6, "requires": { @@ -24,13 +24,21 @@ "markers": "python_version >= '3.6'", "version": "==5.2.0" }, + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, "asgiref": { "hashes": [ - "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", - "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed" + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" ], - "markers": "python_version >= '3.7'", - "version": "==3.7.2" + "markers": "python_version >= '3.8'", + "version": "==3.8.1" }, "attrs": { "hashes": [ @@ -42,12 +50,12 @@ }, "awscli": { "hashes": [ - "sha256:498ce59cdaa8445eb19e8520ece5345c5cf69001c3f3a0824b1b64292b794986", - "sha256:9f4510a771ce65f3ccc271b946facb84a74b450ce3e1089627dccaf9eba3006b" + "sha256:86dd1d4c9ad6f26200ce76825ce75c14dc06d7918beac11384c2452936984b42", + "sha256:9cefbe656c2422efde10c40b2eefd82bef02f86da44042882427df2529f8cf53" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.32.64" + "version": "==1.32.69" }, "beautifulsoup4": { "hashes": [ @@ -77,19 +85,19 @@ }, "boto3": { "hashes": [ - "sha256:8c6fbd3d45399a4e4685010117fb2dc52fc6afdab5a9460957d463ae0c2cc55d", - "sha256:e5d681f443645e6953ed0727bf756bf16d85efefcb69cf051d04a070ce65e545" + "sha256:2e25ef6bd325217c2da329829478be063155897d8d3b29f31f7f23ab548519b1", + "sha256:898a5fed26b1351352703421d1a8b886ef2a74be6c97d5ecc92432ae01fda203" ], "markers": "python_version >= '3.8'", - "version": "==1.34.64" + "version": "==1.34.69" }, "botocore": { "hashes": [ - "sha256:084f8c45216d62dc1add2350e236a2d5283526aacd0681e9818b37a6a5e5438b", - "sha256:0ab760908749fe82325698591c49755a5bb20307d85a419aca9cc74e783b9407" + "sha256:d1ab2bff3c2fd51719c2021d9fa2f30fbb9ed0a308f69e9a774ac92c8091380a", + "sha256:d3802d076d4d507bf506f9845a6970ce43adc3d819dd57c2791f5c19ed6e5950" ], "markers": "python_version >= '3.8'", - "version": "==1.34.64" + "version": "==1.34.69" }, "cachecontrol": { "hashes": [ @@ -497,12 +505,12 @@ }, "djangorestframework": { "hashes": [ - "sha256:3f4a263012e1b263bf49a4907eb4cfe14de840a09b1ba64596d01a9c54835919", - "sha256:5fa616048a7ec287fdaab3148aa5151efb73f7f8be1e23a9d18484e61e672695" + "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6", + "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.15.0" + "version": "==3.15.1" }, "docutils": { "hashes": [ @@ -558,27 +566,27 @@ "grpc" ], "hashes": [ - "sha256:610c5b90092c360736baccf17bd3efbcb30dd380e7a6dc28a71059edb8bd0d8e", - "sha256:9df18a1f87ee0df0bc4eea2770ebc4228392d8cc4066655b320e2cfccb15db95" + "sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6", + "sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==2.17.1" + "version": "==2.18.0" }, "google-api-python-client": { "hashes": [ - "sha256:77447bf2d6b6ea9e686fd66fc2f12ee7a63e3889b7427676429ebf09fcb5dcf9", - "sha256:a5953e60394b77b98bcc7ff7c4971ed784b3b693e9a569c176eaccb1549330f2" + "sha256:1c2bcaa846acf5bac4d6f244d8373d4de9de73d64eb6e77b56767ab4cf681419", + "sha256:a17226b02f71de581afe045437b441844110a9cd91580b73549d41108cf1b9f0" ], "markers": "python_version >= '3.7'", - "version": "==2.122.0" + "version": "==2.123.0" }, "google-auth": { "hashes": [ - "sha256:80b8b4969aa9ed5938c7828308f20f035bc79f9d8fb8120bf9dc8db20b41ba30", - "sha256:9fd67bbcd40f16d9d42f950228e9cf02a2ded4ae49198b27432d0cded5a74c38" + "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", + "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415" ], "markers": "python_version >= '3.7'", - "version": "==2.28.2" + "version": "==2.29.0" }, "google-auth-httplib2": { "hashes": [ @@ -605,11 +613,11 @@ }, "google-cloud-storage": { "hashes": [ - "sha256:5d9237f88b648e1d724a0f20b5cde65996a37fe51d75d17660b1404097327dd2", - "sha256:7560a3c48a03d66c553dc55215d35883c680fe0ab44c23aa4832800ccc855c74" + "sha256:91a06b96fb79cf9cdfb4e759f178ce11ea885c79938f89590344d079305f5852", + "sha256:dda485fa503710a828d01246bd16ce9db0823dc51bbca742ce96a6817d58669f" ], "markers": "python_version >= '3.7'", - "version": "==2.15.0" + "version": "==2.16.0" }, "google-crc32c": { "hashes": [ @@ -1159,6 +1167,100 @@ ], "version": "==2.21" }, + "pydantic": { + "hashes": [ + "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6", + "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.6.4" + }, + "pydantic-core": { + "hashes": [ + "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a", + "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed", + "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979", + "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff", + "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5", + "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45", + "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340", + "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad", + "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23", + "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6", + "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7", + "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241", + "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda", + "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187", + "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba", + "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c", + "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2", + "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c", + "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132", + "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf", + "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972", + "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db", + "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade", + "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4", + "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8", + "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f", + "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9", + "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48", + "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec", + "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d", + "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9", + "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb", + "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4", + "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89", + "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c", + "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9", + "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da", + "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac", + "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b", + "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf", + "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e", + "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137", + "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1", + "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b", + "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8", + "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e", + "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053", + "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01", + "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe", + "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd", + "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805", + "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183", + "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8", + "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99", + "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820", + "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074", + "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256", + "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8", + "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975", + "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad", + "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e", + "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca", + "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df", + "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b", + "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a", + "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a", + "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721", + "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a", + "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f", + "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2", + "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97", + "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6", + "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed", + "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc", + "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1", + "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe", + "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120", + "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f", + "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a" + ], + "markers": "python_version >= '3.8'", + "version": "==2.16.3" + }, "pyjwt": { "extras": [ "crypto" @@ -1394,11 +1496,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:4a8364b8f7edbf47f95f7163e48334c96100d9c098f0ae6606e2e18183c223e6", - "sha256:a654ee7e497a3f5f6368b36d4f04baeab1fe92b3105f7f6965d6ef0de35a9ba4" + "sha256:41df73af89d22921d8733714fb0fc5586c3461907e06688e6537d01a27e0e0f6", + "sha256:8d768724839ca18d7b4c7463ef7528c40b7aa2bfbf7fe554d5f9a7c044acfd36" ], "index": "pypi", - "version": "==1.42.0" + "version": "==1.43.0" }, "six": { "hashes": [ @@ -1433,6 +1535,14 @@ "markers": "python_version >= '3.7'", "version": "==4.66.2" }, + "typing-extensions": { + "hashes": [ + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + ], + "markers": "python_version >= '3.8'", + "version": "==4.10.0" + }, "tzdata": { "hashes": [ "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", diff --git a/ara/controller/__init__.py b/ara/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/controller/api.py b/ara/controller/api.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/controller/authentication.py b/ara/controller/authentication.py new file mode 100644 index 00000000..017a67e5 --- /dev/null +++ b/ara/controller/authentication.py @@ -0,0 +1,3 @@ +class AuthLoggedInUser: + # TODO + pass diff --git a/ara/controller/constants.py b/ara/controller/constants.py new file mode 100644 index 00000000..66b0f960 --- /dev/null +++ b/ara/controller/constants.py @@ -0,0 +1,65 @@ +from enum import IntEnum, unique + + +@unique +class HttpStatusCode(IntEnum): + CONTINUE = 100 + SWITCHING_PROTOCOLS = 101 + OK = 200 + CREATED = 201 + ACCEPTED = 202 + NON_AUTHORITATIVE_INFORMATION = 203 + NO_CONTENT = 204 + RESET_CONTENT = 205 + PARTIAL_CONTENT = 206 + MULTI_STATUS = 207 + ALREADY_REPORTED = 208 + IM_USED = 226 + MULTIPLE_CHOICES = 300 + MOVED_PERMANENTLY = 301 + FOUND = 302 + SEE_OTHER = 303 + NOT_MODIFIED = 304 + USE_PROXY = 305 + RESERVED = 306 + TEMPORARY_REDIRECT = 307 + PERMANENT_REDIRECT = 308 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + PAYMENT_REQUIRED = 402 + FORBIDDEN = 403 + NOT_FOUND = 404 + METHOD_NOT_ALLOWED = 405 + NOT_ACCEPTABLE = 406 + PROXY_AUTHENTICATION_REQUIRED = 407 + REQUEST_TIMEOUT = 408 + CONFLICT = 409 + GONE = 410 + LENGTH_REQUIRED = 411 + PRECONDITION_FAILED = 412 + REQUEST_ENTITY_TOO_LARGE = 413 + REQUEST_URI_TOO_LONG = 414 + UNSUPPORTED_MEDIA_TYPE = 415 + REQUESTED_RANGE_NOT_SATISFIABLE = 416 + EXPECTATION_FAILED = 417 + IM_A_TEAPOT = 418 + UNPROCESSABLE_ENTITY = 422 + LOCKED = 423 + FAILED_DEPENDENCY = 424 + UPGRADE_REQUIRED = 426 + PRECONDITION_REQUIRED = 428 + TOO_MANY_REQUESTS = 429 + REQUEST_HEADER_FIELDS_TOO_LARGE = 431 + UNAVAILABLE_FOR_LEGAL_REASONS = 451 + INTERNAL_SERVER_ERROR = 500 + NOT_IMPLEMENTED = 501 + BAD_GATEWAY = 502 + SERVICE_UNAVAILABLE = 503 + GATEWAY_TIMEOUT = 504 + HTTP_VERSION_NOT_SUPPORTED = 505 + VARIANT_ALSO_NEGOTIATES = 506 + INSUFFICIENT_STORAGE = 507 + LOOP_DETECTED = 508 + BANDWIDTH_LIMIT_EXCEEDED = 509 + NOT_EXTENDED = 510 + NETWORK_AUTHENTICATION_REQUIRED = 511 diff --git a/ara/controller/pagination.py b/ara/controller/pagination.py new file mode 100644 index 00000000..5cb0b169 --- /dev/null +++ b/ara/controller/pagination.py @@ -0,0 +1,14 @@ +from typing import Any + +from pydantic import BaseModel + + +class PaginatedData(BaseModel): + count: int + next: str | None + previous: str | None + results: list[Any] + + +class NesAraPagination: + pass diff --git a/ara/controller/request.py b/ara/controller/request.py new file mode 100644 index 00000000..375a818e --- /dev/null +++ b/ara/controller/request.py @@ -0,0 +1,10 @@ +from typing import TypeVar + +from django.contrib.auth import get_user_model +from django.http import HttpRequest + +User = TypeVar("User", bound=get_user_model()) + + +class LoggedInUserRequest(HttpRequest): + user: User diff --git a/ara/controller/response.py b/ara/controller/response.py new file mode 100644 index 00000000..4909e2a2 --- /dev/null +++ b/ara/controller/response.py @@ -0,0 +1,21 @@ +from http import HTTPStatus +from typing import Any, NamedTuple + +from pydantic import BaseModel + + +class AraResponse(NamedTuple): + status_code: HTTPStatus + data: Any + + +class AraErrorResponseBody(BaseModel): + # necessary + error_code: int | None + error_reason: str = "" + + def __init__(self, exception: Exception): + message = str(exception) + # TODO: Create AraException and use error_code & error_reason + data = {"message": message} + super().__init__(**data) diff --git a/ara/controller/urls.py b/ara/controller/urls.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/domain/__init__.py b/ara/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/domain/ara_entity.py b/ara/domain/ara_entity.py new file mode 100644 index 00000000..1cac57aa --- /dev/null +++ b/ara/domain/ara_entity.py @@ -0,0 +1,35 @@ +from typing import Any + +from pydantic import BaseModel, PrivateAttr + + +class AraEntity(BaseModel): + _updated_fields: set[str] = PrivateAttr(set()) + + @property + def updated_fields(self): + return self._updated_fields + + @property + def updated_values(self) -> dict[str, Any]: + return {key: getattr(self, key) for key in self._updated_fields} + + def set_attribute(self, field_name: str, value: Any): + self.__dict__[field_name] = value + is_private_field = field_name.startswith("_") or field_name.startswith("__") + if is_private_field is False: + self._updated_fields.add(field_name) + + def __setattr__(self, name: str, value: Any) -> None: + is_private_field = name.startswith("_") or name.startswith("__") + if is_private_field is False: + self._updated_fields.add(name) + return super().__setattr__(name, value) + + class Config: + arbitrary_types_allowed = True + + +class AraEntityCreateInput(BaseModel): + class Config: + arbitrary_types_allowed = True diff --git a/ara/infra/__init__.py b/ara/infra/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ara/infra/django_infra.py b/ara/infra/django_infra.py new file mode 100644 index 00000000..b0575093 --- /dev/null +++ b/ara/infra/django_infra.py @@ -0,0 +1,150 @@ +from typing import Any, Generic, Type, TypeVar + +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db.models import Model + +from ara.domain.ara_entity import AraEntity, AraEntityCreateInput + +T = TypeVar("T", bound=Model) + + +class AraDjangoInfra(Generic[T]): + def __init__(self, model: Type[T]) -> None: + self.model = model + + def get_by_id(self, id: Any, *, is_select_for_update: bool = False) -> T: + """ + Generic function for simple get by id queries. + + Args: + id (Any): + Not sure of the id type. It could be hash Id or int. + TODO(hyuk): check for the all models. + is_select_for_update (bool): + Set True if get queryset for update purpose. Defaults to False. + + """ + if is_select_for_update: + return self.model.objects.select_for_update().get(id=id) + return self.model.objects.get(id=id) + + def get_filtered_objects( + self, + *, + columns: list[str] | None = None, + conditions: dict[str, Any], + is_select_for_update: bool = False, + ) -> list[T | dict[str, Any]]: + """ + Generic function for simple queries. + Should not be used for complex & specific purpose queries. + + Args: + columns (List[str] | None): + List of column names to fetch. Get all columns if None. Default None. + conditions (Dict[str, Any]): + Dictionary of field names and their corresponding values to filter by. + is_select_for_update (bool): + Set True if get queryset for update purpose. Defaults to False. + + Returns: + List[T | Dict[str, Any]]: + A list containing the matching object, + with only the specified columns if `columns` is not None. + + Example: + # Get all rows with id=1 and only fetch 'id' and 'name' fields + query1 = get_filtered_queryset( + columns=['id', 'name'], + conditions={'id': 1} + ) + + # Get the first 10 rows with rating>=4.0 and order by created_at descending + query2 = get_filtered_queryset( + columns=['id', 'name', 'rating', 'created_at'], + conditions={'rating__gte': 4.0} + ).order_by('-created_at').limit(10) + + Raises: + ValidationError: If conditions parameter is empty or invalid. + """ + if not conditions: + raise ValidationError("conditions parameter is required") + + try: + if is_select_for_update: + queryset = self.model.objects.select_for_update() + else: + queryset = self.model.objects + + queryset = queryset.filter(**conditions) + + if columns is not None: + queryset = queryset.values(*columns) + + return list(queryset) + + except ValidationError: + raise ValidationError("invalid conditions parameter") + + def create_manual(self, **kwargs) -> T: + return self.model.objects.create(**kwargs) + + def update_or_create(self, **kwargs) -> tuple[T, bool]: + return self.model.objects.update_or_create(**kwargs) + + def get_by(self, **kwargs) -> T | None: + """Returns repository model instance if exists. + + :param kwargs: keyword arguments of fields + :raises MultipleObjectsXxx: when multiple rows exist + :return: None or model instance if exists. + """ + try: + return self.model.objects.get(**kwargs) + except ObjectDoesNotExist: + return None + + def _to_model(self, entity: AraEntity) -> Model: + raise NotImplementedError() + + @staticmethod + def convert_model_to_entity(model: T) -> AraEntity: + raise NotImplementedError() + + def _convert_entity_to_model(self, entity: AraEntity) -> Model: + raise NotImplementedError() + + def _convert_create_input_to_model(self, create_input: AraEntityCreateInput) -> T: + raise NotImplementedError() + + def bulk_update_entity(self, entities: list[AraEntity]): + if len(entities) == 0: + return + + model_instances = [self._convert_entity_to_model(entity) for entity in entities] + + unique_updated_fields = list( + {field for entity in entities for field in entity.updated_fields} + ) + if len(unique_updated_fields) == 0: + return + + self.model.objects.bulk_update(model_instances, unique_updated_fields) + + def bulk_update(self, instances: list[T], fields: list[str]): + return self.model.objects.bulk_update(instances, fields) + + def bulk_create(self, inputs: list[AraEntityCreateInput]) -> list[AraEntity]: + instances = [self._convert_create_input_to_model(input) for input in inputs] + created_instances = self.model.objects.bulk_create(instances) + entities = [ + self.convert_model_to_entity(created_instance) + for created_instance in created_instances + ] + return entities + + def save_entity(self, entity: AraEntity): + model = self._convert_entity_to_model(entity) + model.save() + return entity diff --git a/ara/service/__init__.py b/ara/service/__init__.py new file mode 100644 index 00000000..e69de29b