{"id":17340339,"url":"https://github.com/mariotoffia/pypwext","last_synced_at":"2026-04-28T22:34:28.099Z","repository":{"id":62582772,"uuid":"436606974","full_name":"mariotoffia/pypwext","owner":"mariotoffia","description":"AWS Python Lambda Power tools Extensions and Decorators","archived":false,"fork":false,"pushed_at":"2021-12-22T12:27:06.000Z","size":84,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-25T08:39:45.134Z","etag":null,"topics":["aws","aws-lambda","error-handling","framework","http","lambda","logging","python","python3","service"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mariotoffia.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-12-09T12:22:34.000Z","updated_at":"2021-12-22T12:24:25.000Z","dependencies_parsed_at":"2022-11-03T21:36:34.934Z","dependency_job_id":null,"html_url":"https://github.com/mariotoffia/pypwext","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/mariotoffia/pypwext","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariotoffia%2Fpypwext","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariotoffia%2Fpypwext/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariotoffia%2Fpypwext/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariotoffia%2Fpypwext/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mariotoffia","download_url":"https://codeload.github.com/mariotoffia/pypwext/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariotoffia%2Fpypwext/sbom","scorecard":{"id":619360,"data":{"date":"2025-08-11","repo":{"name":"github.com/mariotoffia/pypwext","commit":"8a7ad891ade34df84f040494d8176f8095d0fd3c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.6,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":2,"reason":"dependency not pinned by hash detected -- score normalized to 2","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/build.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:49: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/push.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/push.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/push.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/push.yml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/push.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/release.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/mariotoffia/pypwext/release.yml/master?enable=pin","Info:   0 out of   9 GitHub-owned GitHubAction dependencies pinned","Info:   1 out of   2 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/build.yml:1","Warn: no topLevel permission defined: .github/workflows/push.yml:1","Warn: no topLevel permission defined: .github/workflows/release.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-21T04:51:19.150Z","repository_id":62582772,"created_at":"2025-08-21T04:51:19.150Z","updated_at":"2025-08-21T04:51:19.150Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32402670,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T19:38:08.556Z","status":"ssl_error","status_checked_at":"2026-04-28T19:37:55.688Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","aws-lambda","error-handling","framework","http","lambda","logging","python","python3","service"],"created_at":"2024-10-15T15:44:41.064Z","updated_at":"2026-04-28T22:34:28.061Z","avatar_url":"https://github.com/mariotoffia.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PyPwExt\n\n![example workflow](https://github.com/mariotoffia/pypwext/actions/workflows/push.yml/badge.svg)\n[![PyPI version](https://badge.fury.io/py/pypwext.svg)](https://badge.fury.io/py/pypwext)\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=pypwext\u0026metric=alert_status)](https://sonarcloud.io/dashboard?id=pypwext)\n\nThis is a extension, decorators and other utilities that complements thew [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python) library. It is centered around the following four **pillars**\n\n\n1. Logging\n```python\nlogger = PyPwExtLogger()\n\n@logger.method(classification=InfoClassification.PII)\ndef subscribe(customer: Customer) -\u003e Subscription:\n    ...\n```\n2. Error handling\n```python\nerrors = PyPwExtErrorHandler()\n\n@errors.collect\ndef do_operation():\n    raise PyPwExtHTTPError('Something went wrong', details={'foo': 'bar'})\n```\n3. HTTP and Lambda Communication\n```python\nhttp = PyPwExtHTTPSession() # Defaults with \"sane\" retries, exp back-off etc.\n\n@http.method(\n    method='POST',\n    url='https://{STAGE}.execute-api.{AWS_REGION}.amazonaws.com/{api_version}/my-api'\n    params= {'email':'{email}'},\n    body='customer'\n)\ndef send_offer(\n    api_version:str, \n    email: str, \n    customer: Customer,\n    response_code:HTTPStatus=HTTPStatus.OK, \n    response_body:str = ''\n) -\u003e str:\n    ...\n```\n4. µService Support\n```python\nservice = PyPwExtService()\n\n@service.response\n@event_parser(model=Order)\ndef handler(event: Order, context: LambdaContext):\n\n    if not event.pypwext_id:\n        raise StdPyPwExtError(\n            code=HTTPStatus.BAD_REQUEST,\n            message=\"Missing pypwext_id\",\n        )\n\n    return PyPwExtResponse(\n        status_code=HTTPStatus.OK,\n        updated=[item for item in event.items],\n        operation=\"create-order\",            \n    )\n```\n\n\n## Overview\nThis module contains the core functionality of the PyPwExt µService applications. \n\n:bulb: Each piece of functionality is implemented as an individual function and is **not** \ndependant on each other. Hence, it is up to the implementor to **choose** which ones to use.\n\nIt consists of **structural** and **semantic** elements to align the *µServices* when logging, \nerror, input and output handling. It does **not** mandate any specific model when it comes to \nthe content of the payloads sent back and forth. \n\nThe only **reserved** return body element is *error*. But that is if you use `PyPwExtResponse` \n**and** `PyPwExtService.response` decorator. You may even omit that if you set the \n`@response(just_status_code=True)`, and that is the *default* behavior 😊\n\nFor example:\n```python\nservice = PyPwExtService()\nlogger = PyPwExtLogger(service='my-service')\n\n@logger.method(classification=InfoClassification.PII)\n@service.response\ndef my_service(request):\n    raise PyPwExtHTTPError(\n        message='bad input',\n        details={\n            Operation: 'my-operation'\n            'org_no': '7112234455'\n        })\n```\n\nBy default, this would return the following\n```http\nHTTP status code: 400\nBody: None\n```\n\nIf you instead set \n\n```python\n@service.response(just_status_code=False)\n```\n\nit would return\n\n```json\n{\n    \"error\": {\n        \"message\": \"bad input\",\n        \"details\": {\n            \"operation\": \"my-operation\",\n            \"org_no\": \"7112234455\"\n        }\n    }\n}\n```\n\n## A extended example\n\nThe example below uses most of the features of the *PyPwExt* main types. Even though each feature supports quite a few configuration options to fit the exact purpose, it comes with many *\"sane\"* defaults. Hence, many variations can be avoided. But it is there when it is needed.\n\n```python\nlogger = PyPwExtLogger(default_logger=True, service='my-service')\nerrors = PyPwExtErrorHandler()\nservice = PyPwExtService()\nhttp = PyPwExtHTTPSession() # Defaults with \"sane\" retries, exp back-off etc.\n\nclass Customer(BaseMode, extra=Extra.allow)\n    email: str\n    age: Optional[int] = None\n\nclass Customers(BaseModel, extra=Extra.allow):\n    Customers: List[Customer] = Field(default_factory=list)\n\n\n@errors.collect\n@logger.method(log_exception=False)\n@http.method(\n    method='POST',\n    url='https://{STAGE}.execute-api.{AWS_REGION}.amazonaws.com/{api_version}/my-api'\n    params= {'email':'{email}'},\n    body='customer'\n)\ndef send_offer(\n    api_version:str, \n    email: str, \n    customer: Customer,\n    response_code:HTTPStatus=HTTPStatus.OK, \n    response_body:str = ''\n) -\u003e str:\n\n    # send offer to customer -\u003e fail -\u003e raise\n    if response_code == HTTPStatus.OK:\n        raise PyPwExtErrorWithReturn(\n            code=HTTPStatus.BAD_REQUEST,\n            message=f\"Failed to send offer to customer: {email}\",\n            details={Operation: 'send_offer', 'customer': customer}\n        )\n\n    return email\n\n@logger.method(type=LogEntryType.AUDIT, operation='send_offer')\n@errors.collect(root=True)\n@service.response\n@event_parser(model=CustomerData)\ndef handler(event: CustomerData, ctx: LambdaContext) -\u003e any:\n    return PyPwExtResponse(\n        status_code=HTTPStatus.OK,\n        updated=list(filter(partial(is_not, None), [send_offer('1.0.0', c.email,c) for c in event])),\n        operation=\"create-offer\"\n    )\n```\n\nSince, `@service.response` by default returns the \"largest\" status code (*otherwise `code_from_error=False`*). The above could, for example, produce the following response payload:\n\n```json\n{\n    \"updated\": [\"nisse@manpower.com\"], \n    \"operation\": \"create-offer\", \n    \"error\": [\n        {\n            \"code\": 400, \n            \"msg\": \"Failed to find record for customer: mario.toffia@pypwext.se\", \n            \"classification\": \"NA\", \n            \"details\": {\"operation\": \"send_offer\", \"customer\": { \"email\": \"mario.toffia@pypwext.se\", \"org_no\": \"1234567890\" }}\n        }, \n        {\n            \"code\": 400, \n            \"msg\": \"Failed to find record for customer: ivar@ikea.se\", \n            \"classification\": \"NA\", \n            \"details\": {\"operation\": \"send_offer\", \"customer\": { \"email\": \"ivar@ikea.se\", \"org_no\": \"0987654321\" }}\n        }\n    ]\n}\n```\n\nSince `@logger.method` is applied, it logs it using `DEBUG` verbosity at the entry, and `INFO` log the exit by default. However, it won't log the exception in the `send_offer` function since it would fill the log with cluttered information.\n\nThe `@http.method` automatically detects *AWS API* endpoints and uses *SigV4* by default. However, it is completely configurable in the `PyPwExtHTTPSession` constructor. \n\nSince no argument given in ``@service.response``, the ``PyPwExtResponse`` object is converted to an API Gateway response that supports either *REST* or *HTTPv2* version of the API Gateway. Thus the *JSON* payload is the *body* part of the response, and the *statusCode* is set to 402 (*NOT FOUND*) since that was the last collected error. Of course, you may override this behaviour if you want.\n\nThe *PyPwExt* also comes with an out of the box ``PyPwExtJSONEncoder``capable of handling many object types and\nis extensible. For example, it adheres to ``base`` module protocols such as ``SupportsToJson`` and ``SupportsToCuratedDict`` (that also ``pydantic.BaseModel`` also exposes).\n\n### A note on the ordering of the decorators\n\nHow decorators wrap the functions is basic *Python* knowledge. However, it is sometimes essential which order the function is decorated.\n\n```python\nfrom aws_lambda_powertools.utilities.data_classes import (\n    event_source, \n    APIGatewayProxyEventV2\n)\n\n@event_source(data_class=APIGatewayProxyEventV2)\n@logger.method\n@errors.collect(root=True)\n@service.response(just_status_code=False)\ndef handler(event: APIGatewayProxyEventV2,, context: LambdaContext):\n    ...\n```\n\n1.  First, install a *root* collector so the `@service.response` may pick those errors and merge those with the response. \n    Therefore, the `@errors.collect` must be before (thus installs the collector for the `@service.response` to use).\n\n2.  The intention of `@logger.method` is to log the `APIGatewayProxyEventV2` in addition to the `Response` object. It also logs\n    any raised `PyPwExtError` object.\n\n3.  Then, for the `@event_source` to get its data, parse it to an `APIGatewayProxyEventV2` object.\n\n\n:bulb: **Hence, the decorator *\"execution\"* order is top-down and returns bottom-up. You decide what you want to happen!**\n\n## Sample Typed µService\n\n```python\nclass PyPwExtModel(BaseModel): # NOTE: You may use the @dataclass decorator instead of the BaseModel class\n    pypwext_id: str\n    \"\"\"Unique trace id, to be passed in all systems and logged to make trails\"\"\"\n\nclass OrderItem(BaseModel):\n    id: int\n    quantity: int\n    description: str\n\nclass Order(PyPwExtModel):\n    id: int\n    description: str\n    items: List[OrderItem]\n    optional_field: Optional[str]\n\nlogger = PyPwExtLogger()\nmetrics = Metrics() \nservice = PyPwExtService()\n\n@service.response\n@logger.method\n@metrics.log_metrics\n@event_parser(model=Order)\ndef handler(event: Order, context: LambdaContext):\n\n    if not event.pypwext_id:\n        raise StdPyPwExtError(\n            code=HTTPStatus.BAD_REQUEST,\n            message=\"Missing pypwext_id\",\n        )\n\n    return PyPwExtResponse(\n        status_code=HTTPStatus.OK,\n        updated=[item for item in event.items],\n        operation=\"create-order\",            \n    )\n```\n\nThe sample renders an API Gateway *body* response of:\n```json\n{\n    \"operation\": \"create-order\",\n    \"updated\": [{\"id\": 1015938732, \"quantity\": 1, \"description\": \"item xpto\"}]\n}\n```\n\nIt logs the objects in the payload and the response, and if any error occurs, if exception-log it automatically. It also records metrics for *my-service* in the namespace *my-namespace* as an example of setting the environment variables *POWERTOOLS_SERVICE_NAME* and *POWERTOOLS_METRICS_NAMESPACE* to *my-service* and *my-namespace* respectively.\n\n### Sample Calling other HTTP endpoints\n\nThe *HTTP* module returns a pre-configured HTTP session that defaults. For example, it can configure the number of retries and the timeout and which methods and response codes should yield a retry.\n\nWhen retrying, it uses an exponential back-off and handles temporal outages.\n\n```python\nwith PyPwExtHTTPSession(logger=logger) as http:\n    response = http.get(\n        'https://api.openaq.org/v1/cities',\n        params={'country': 'SE'}\n    )\n```\n\nBelow, do reconfigure the timeout and logs on request and response. It also reconfigures the number of retries to 3 and a higher back-off factor (wait longer time).\n\n```python\nlogger = PyPwExtLogger()\n\nwith PyPwExtHTTPSession(\n    PyPwExtHTTPAdapter(timeout=10, logger=logger),\n    PyPwExtRetry(total=3, backoff_factor=2),\n) as http:\n    response = http.get(\n        \"https://en.wikipedia.org/w/api.php\"\n    )\n```\n\nIf an *API Gateway* call is wanted. It is expected to be on the following form: *https://{api-gateway-id}.execute-api.{region}.amazonaws.com/....*. The *HTTP* module automatically configures a `BotoAWSRequestsAuth` and set it to the *request.auth* parameter using the *AWS_REGION* environment variable. Thus, this makes the request match a *SigV4* request and gets authenticated at the *API Gateway* using the *AWS_ACCESS_KEY_ID* and *AWS_SECRET_ACCESS_KEY*.\n\n```python\n    with PyPwExtHTTPSession(region='eu-north-1') as http:\n    http.get(\n        'https://abc123.execute-api.eu-north-1.amazonaws.com/dev/cities',\n        params={'country': 'SE'}\n    )\n```\n\nThe above example overrides the *AWS_REGION* environment to *eu-north-1*.\n\n**NOTE: Since the `PyPwExtHTTPSession` is a standard python library `HTTPSession`, it pools connections and so on and thus should be\ncached to avoid the overhead of creating a new session for each request.**\n\nThe `PyPwExtHTTPSession` extension handles synchronous and asynchronous lambda calls with configurable retries and \"sensible\" defaults. It is possible to use them from code or decorated on the function.\n\nThe sample below invokes the lambda synchronously and passes custom parameters in the `LambdaContext` and nothing as the body. It has a default of 10 times before giving up.\n\n```python\nwith PyPwExtHTTPSession() as http:\n    result = http.func(url='mario-unit-test-function',params={'country': 'SE'})\n```\n\n#### HTTP Decorator Samples\n\nIt is possible to decorate a function with the *HTTP* decorator to make the *HTTP* call and process the response in method or make the decorator return the response. The latter automatically raises a `PyPwExtHTTPError` if the *HTTP* status code is 299 or greater.\n\nThe simplest one is to make the request declarative with no processing in function (i.e. not declare any of the `response`, `response_body` or `response_code` parameters in the function prototype).\n\n```python\nhttp = PyPwExtHTTPSession()\n\n@http.method(url='https://{STAGE}.api.openaq.org/v1/cities', params={'country': '{country}'})\ndef cities(country:str) -\u003e requests.Response:\n    pass\n\ntry\n    response = cities(country='SE') # This will call the site and return the response\n    print(response.text)\nexcept PyPwExtHTTPError as e:\n    print(e.response_body)\n```\n\nIt is possible to process the response in the function and manually handle errors. Supply with any of the \"response\" parameters. Make sure the declare the \"response\" parameters with defaults (e.g. `response = None`).\n\n```python\nhttp = PyPwExtHTTPSession()\n\n@http.method(url='https://api.openaq.org/v1/cities', params={'country': '{country}'})\ndef cities(country:str, response_body:str = '', response_code:HTTPStatus = HTTPStatus.NOT_FOUND) -\u003e str:\n    if response_code == http.HTTPStatus.OK:\n        return f'{country} has {response_body[\"count\"]} cities'\n    else:\n        raise PyPwExtHTTPError(code=response_code, message=response_body)\n```\n\nThe URL is formatted with parameters supplied to the function when *UPPERCASE* finds those as environment variables. It then makes the\ncall and passes the response to the function. The function may then process the response and return a string.\n\nSince the `PyPwExtHTTPSession` is by default resolving AWS API gateway calls, it uses the `BotoAWSRequestsAuth` to set the credentials by creating a SigV4 request (it is possible to turn off this behaviour). \n\n```python\nhttp = PyPwExtHTTPSession()\n\n@http.method(\n    method='POST',\n    url='https://{gw_id}.execute-api.{AWS_REGION}.amazonaws.com/dev/cities',\n    body='city'\n)\ndef get_cities(gw_id: str, city: str, response: Optional[requests.Response] = None) -\u003e Dict[str, Any]:\n    \"\"\" Gets the cities from a specific country.\n    \n        Args:\n            gw_id:  The API Gateway ID\n            city:   The body containing the country to fetch.\n                    for example: `{\"country\": \"SE\"}`\n\n\n        Returns:\n            The response body.\n    \"\"\"\n    return {\n        'country': country,        \n        'result': json.loads(response.text)\n    }\n\nvalue = get_cities('abc123', 'SE', 'the body')\n```\n\n#### HTTP Decorator Lambda Samples\nIt is possible to invoke the lambda function synchronously (_FUNC_) and asynchronously (_EVENT_). Given the following lambda function\n```python\ndef lambda_handler(event, context):\n    if event.get('country'):\n        country = event['country']\n    else:\n        country = context.client_context.custom['country'] # contrives to show that you can use custom data in the context\n        \n    result = requests.get(\n        'https://api.openaq.org/v1/cities', params={'country':country}\n    )\n    \n    if result.status_code != 200:\n        raise ValueError(f'Failed to get cities result: {result.status_code}')\n    \n    return json.loads(result.text)\n```\n\nInvoking the lambda using _FUNC_ (synchronous) is as simple as:\n```python\n@http.method(\n    method='FUNC',\n    url='arn:aws:lambda:eu-west-1:010711114025:function:mario-unit-test-function',\n    params={'country': '{country}'}\n)\ndef get_cities(country: str, response: LambdaResponse = None) -\u003e str:\n    if response.StatusCode == HTTPStatus.OK.value:\n        return response.payload_as_text()\n    else:\n        raise PyPwExtHTTPError(\n            code=response.StatusCode,\n            message=f'Failed to get cities from {country}',\n            details={\n                'error': response.payload_as_text()\n            }\n        )\n\nvalue = get_cities(country='SE')\n```\n\n:bulb: **NOTE:** The lambda function is invoked synchronously. Hence, the `response` is a `LambdaResponse` object instead of the `requests.Response` object. Therefore, thee returned payload from the lambda.\n\nIf you'd like to invoke the lambda asynchronously, you can use the _EVENT_ method. The following example is for a lambda function that is invoked asynchronously.\n\n```python\n@http.method(method='EVENT', url='{STAGE}-function:mario-unit-test-function', body='body')\ndef get_cities(body: Dict[str, Any], response: LambdaResponse = None) -\u003e str:\n    if response.StatusCode == HTTPStatus.ACCEPTED.value:\n        return json.dumps(response.ResponseMetadata)\n    else:\n        raise PyPwExtHTTPError(\n            code=response.StatusCode,\n            message=f'Failed to get cities from {body}',\n            details={\n                'error': response.FunctionError\n            }\n        )\n\nvalue = get_cities({'country': 'SE'})\n```\n\nSince it is asynchronous, no custom `LambdaContext` can be used. Note that the successful invocation is indicated by the `StatusCode` being `202` (Accepted). Since the lambda cannot return anything, the `LambdaResponse.Payload` do not contain anything.\n\nIt is still possible to use lambda's `response_body` and `response_code` parameters. The `response_body` for _EVENT_ type will be `json.dumps(response.ResponseMetadata)`. When creating a `PyPwExtHTTPSession` it is possible to override the PyPwExt defaults for the boto-core `Config` object. The default is:\n\n```python\nConfig(\n            region_name=self.region, # region is either manual set or AWS_REGION env var\n            connect_timeout=60,\n            read_timeout=60,\n            retries={\n                'total_max_attempts': 10,\n                'max_attempts': 10,\n                'mode': 'adaptive'\n            }\n        )\n```\n\nIf you supply a `Config` object, it is merged over the defaults.\n\n### Sample logging output\n\nThe logger, independent if it is decorated or used directly, forces few new fields\n\n* level\n* timestamp\n* message\n* service\n* classification\n* type\n* operation\n\nAdditionally, if you enable log lambda context, it adds a set of function_* fields. If x-ray is enabled, it logs that as well. The below entry also adds a *correlation_id* of the REST API Gateway request-id.\n\n```python\n@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)\ndef handler(event, context):\n    return app.resolve(event, context)\n```\n\nSample log entry:\n\n```json\n{\n    \"level\": \"ERROR\",\n    \"location\": \"my_func:555\",\n    \"message\": {\n        \"msg\": \"Exception in my_func\",\n        \"args\": {\n            \"person_ssn\": \"19120102-01921\"\n        }\n    },\n    \"timestamp\": \"2021-11-24 13:38:50,673+0000\",\n    \"service\": \"my service\",\n    \"classification\": \"PII\",\n    \"type\": \"AUDIT\",\n    \"cold_start\": true,\n    \"function_name\": \"test_func\",\n    \"function_memory_size\": \"1024\",\n    \"function_arn\": \"arn:aws:lambda:eu-west-1:010711114025:function:test_func\",\n    \"function_request_id\": \"a89efe17-7a9e-49f8-9b4c-f00abd7b2ac8\",\n    \"correlation_id\": \"1db848fb-bf8b-4b5d-b2ad-61e2175181b4\",\n    \"operation\": \"get-user-info\",\n    \"exception\": \"...exception...\",\n    \"exception_name\": \"StdPyPwExtError\",\n    \"xray_trace_id\": \"1-619e4067-62471735a4cf522db5720c7e\"\n}\n```\n\n## Philosophy\n\nThe *PyPwExt* is a µService framework designed to be *opt-in* instead of *opt-out*. It means\nthat you may always choose to use the functionality instead of *REQUIRED* to use it. So, for example, if you\nhave decorated a ``handler`` with `@pypwext_response`, you always have the option to bypass it, e.g. returning\nyour own custom *JSON* response.\n\n```python\nlogger = PyPwExtLogger(service='my-service')\nservice = PyPwExtService()\n\n@logger.method\n@service.response\ndef handler(customers: List[str]):\n    return json.dumps({\n        \"statusCode\": 200,\n        \"body\": {\n            \"updated\": list(filter(partial(is_not, None), [send_offer(c) for c in customers])),\n            Operation: \"create-offer\"\n        }\n    })\n```\n\nThe above still logs if any error occurs and returns a standardized response upon exception but completely bypassing\nresponse handling. Instead, it returns the *JSON* response as-is.\n\nYou may use small parts or the whole package, and each decorator can be used separately. It is also possible to access parts\nfrom code such as the ``@errors.collect`` decorator do expose currently collected errors through. `get_current_collector()` \nwhere actions may be taken due to collected errors down the chain. Or you may choose to clear errors and let the decorator\nprocess as those have never been collected.\n\nThe important part of *PyPwExt* is that the µServices are semantically and structurally the same. Therefore some\nbase types and reserved *keywords* do exist. For example, when a particular operation is commenced, the ``Operation`` key facades the value. For example:\n\n```python\nlogger = PyPwExtLogger(service='my-service')\n\n@logger.method(operation='create-offer')\ndef create_offer(...) -\u003e ...:\n    ...\n```\n\nThe above logs the entry with **DEBUG** and exit with **INFO** level automatically with the `{\"operation\": \"create-offer\"}`.\nIt also uses standardized keywords such as `Return` and `Arguments` to log the entry arguments and return values.\n\nUsing standardized keywords makes it easier to understand and search in the logs.\n\nOf course, the above could be done in code, but not recommended.\n\n```python\nlogger.info({\n        Operation: \"pay\",\n        Classification: InfoClassification.CORPORATE_SENSITIVE_INFO,\n        LogType: LogEntryType.AUDIT,\n        Arguments: {\"amount\": amount, \"credit_card_id\": credit_card_id}})\n```\n\nThe above sample uses constants defined in the ``base`` module. Since the ``PyPwExtLogger`` is based on the Powertools logger, it is quite possible to, e.g. include ``@logger.inject_lambda_context(log_event=True)`` to log the Lambda event including lambda context variables automatically.\n\n## Dependencies\n\nPyPwExt is heavily dependant on the [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/latest/) where the ``PyPwExtLogger`` derives from the Powertools ``Logger`` and ``PyPwExtReturn``understand API Proxy ``Response`` natively.\n\nAn optional dependency that plugs right in is [Pydantic](https://pydantic-docs.helpmanual.io/). It is a model and validation framework that can validate the input and output of the microservice. In addition, the [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/latest/) relies on *Pydantic* when using the [Typing Module](https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/typing/\u003e).\n\n## Setup development environment\n\n1. Create a virtual python environment `python -m venv .venv`.\n2. Activate it by `. .venv/bin/activate`.\n3. Install the dependencies by `make dependencies`.\n\nNow the environment is ready to use.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmariotoffia%2Fpypwext","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmariotoffia%2Fpypwext","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmariotoffia%2Fpypwext/lists"}