diff --git a/.github/contributors.json b/.github/contributors.json index 7a029c764d..013d2c99d8 100644 --- a/.github/contributors.json +++ b/.github/contributors.json @@ -1473,5 +1473,10 @@ "name": "Jakub Boukal", "github_login": "SukiCZ", "twitter_username": "" + }, + { + "name": "Christian Jauvin", + "github_login": "cjauvin", + "twitter_username": "" } ] \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cbfb07a3b..7886315549 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,7 @@ jobs: {{cookiecutter.project_slug}}/requirements/local.txt - name: Install dependencies run: pip install -r requirements.txt - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18" - name: Bare Metal ${{ matrix.script.name }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 832ec45157..c017104991 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: detect-private-key - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.0.3" + rev: "v3.1.0" hooks: - id: prettier args: ["--tab-width", "2"] @@ -30,7 +30,7 @@ repos: exclude: hooks/ - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.11.0 hooks: - id: black diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac2daf9e0..b966784173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,167 @@ All enhancements and patches to Cookiecutter Django will be documented in this f +## 2023.11.14 + + +### Updated + +- Update sentry-sdk to 1.35.0 ([#4681](https://github.com/cookiecutter/cookiecutter-django/pull/4681)) + +- Auto-update pre-commit hooks ([#4683](https://github.com/cookiecutter/cookiecutter-django/pull/4683)) + +## 2023.11.11 + + +### Updated + +- Update celery to 5.3.5 ([#4678](https://github.com/cookiecutter/cookiecutter-django/pull/4678)) + +## 2023.11.09 + + +### Updated + +- Auto-update pre-commit hooks ([#4673](https://github.com/cookiecutter/cookiecutter-django/pull/4673)) + +- Update black to 23.11.0 ([#4674](https://github.com/cookiecutter/cookiecutter-django/pull/4674)) + +## 2023.11.08 + + +### Updated + +- Update pytest-django to 4.7.0 ([#4672](https://github.com/cookiecutter/cookiecutter-django/pull/4672)) + +## 2023.11.06 + + +### Changed + +- Add `rmbackup` script to remove backups from `postgres/backups`. Fixes: #4663 ([#4664](https://github.com/cookiecutter/cookiecutter-django/pull/4664)) + +### Updated + +- Update django-allauth to 0.58.2 ([#4667](https://github.com/cookiecutter/cookiecutter-django/pull/4667)) + +- Update uvicorn to 0.24.0.post1 ([#4666](https://github.com/cookiecutter/cookiecutter-django/pull/4666)) + +## 2023.11.04 + + +### Updated + +- Update uvicorn to 0.24.0 ([#4665](https://github.com/cookiecutter/cookiecutter-django/pull/4665)) + +## 2023.11.03 + + +### Updated + +- Update flake8-isort to 6.1.1 ([#4662](https://github.com/cookiecutter/cookiecutter-django/pull/4662)) + +## 2023.11.02 + + +### Updated + +- Update sentry-sdk to 1.34.0 ([#4660](https://github.com/cookiecutter/cookiecutter-django/pull/4660)) + +## 2023.11.01 + + +### Updated + +- Update django to 4.2.7 ([#4658](https://github.com/cookiecutter/cookiecutter-django/pull/4658)) + +- Update django-stubs to 4.2.6 ([#4657](https://github.com/cookiecutter/cookiecutter-django/pull/4657)) + +## 2023.10.31 + + +### Updated + +- Update pytest-django to 4.6.0 ([#4656](https://github.com/cookiecutter/cookiecutter-django/pull/4656)) + +- Update pytest to 7.4.3 ([#4654](https://github.com/cookiecutter/cookiecutter-django/pull/4654)) + +- Update werkzeug to 3.0.1 ([#4655](https://github.com/cookiecutter/cookiecutter-django/pull/4655)) + +- Update sentry-sdk to 1.33.1 ([#4653](https://github.com/cookiecutter/cookiecutter-django/pull/4653)) + +- Update sentry-sdk to 1.33.0 ([#4652](https://github.com/cookiecutter/cookiecutter-django/pull/4652)) + +- Update crispy-bootstrap5 to 2023.10 ([#4651](https://github.com/cookiecutter/cookiecutter-django/pull/4651)) + +## 2023.10.26 + + +### Updated + +- Update django-anymail to 10.2 ([#4645](https://github.com/cookiecutter/cookiecutter-django/pull/4645)) + +## 2023.10.24 + + +### Updated + +- Update black to 23.10.1 ([#4639](https://github.com/cookiecutter/cookiecutter-django/pull/4639)) + +- Auto-update pre-commit hooks ([#4641](https://github.com/cookiecutter/cookiecutter-django/pull/4641)) + +## 2023.10.23 + + +### Updated + +- Update pylint-django to 2.5.5 ([#4638](https://github.com/cookiecutter/cookiecutter-django/pull/4638)) + +## 2023.10.19 + + +### Updated + +- Update mypy to 1.6.1 ([#4634](https://github.com/cookiecutter/cookiecutter-django/pull/4634)) + +- Update djangorestframework-stubs to 3.14.4 ([#4637](https://github.com/cookiecutter/cookiecutter-django/pull/4637)) + +- Update django-stubs to 4.2.5 ([#4636](https://github.com/cookiecutter/cookiecutter-django/pull/4636)) + +## 2023.10.17 + + +### Updated + +- Auto-update pre-commit hooks ([#4633](https://github.com/cookiecutter/cookiecutter-django/pull/4633)) + +- Update black to 23.10.0 ([#4632](https://github.com/cookiecutter/cookiecutter-django/pull/4632)) + +- Update pillow to 10.1.0 ([#4630](https://github.com/cookiecutter/cookiecutter-django/pull/4630)) + +- Update django-crispy-forms to 2.1 ([#4629](https://github.com/cookiecutter/cookiecutter-django/pull/4629)) + +## 2023.10.13 + + +### Updated + +- Update pre-commit to 3.5.0 ([#4628](https://github.com/cookiecutter/cookiecutter-django/pull/4628)) + +- Update watchfiles to 0.21.0 ([#4627](https://github.com/cookiecutter/cookiecutter-django/pull/4627)) + +## 2023.10.12 + + +### Updated + +- Update django-cors-headers to 4.3.0 ([#4625](https://github.com/cookiecutter/cookiecutter-django/pull/4625)) + +- Update whitenoise to 6.6.0 ([#4624](https://github.com/cookiecutter/cookiecutter-django/pull/4624)) + +- Update sentry-sdk to 1.32.0 ([#4623](https://github.com/cookiecutter/cookiecutter-django/pull/4623)) + +- Bump traefik from 2.10.4 to 2.10.5 ([#4626](https://github.com/cookiecutter/cookiecutter-django/pull/4626)) + ## 2023.10.09 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 070199124b..98d8b32f96 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -509,6 +509,13 @@ Listed in alphabetical order. + + Christian Jauvin + + cjauvin + + + Christopher Clarke diff --git a/docs/docker-postgres-backups.rst b/docs/docker-postgres-backups.rst index c40b6fd69b..fdf4460304 100644 --- a/docs/docker-postgres-backups.rst +++ b/docs/docker-postgres-backups.rst @@ -92,7 +92,15 @@ You will see something like :: Backup to Amazon S3 ---------------------------------- + For uploading your backups to Amazon S3 you can use the aws cli container. There is an upload command for uploading the postgres /backups directory recursively and there is a download command for downloading a specific backup. The default S3 environment variables are used. :: $ docker compose -f production.yml run --rm awscli upload $ docker compose -f production.yml run --rm awscli download backup_2018_03_13T09_05_07.sql.gz + +Remove Backup +---------------------------------- + +To remove backup you can use the ``rmbackup`` command. This will remove the backup from the ``/backups`` directory. :: + + $ docker compose -f local.yml exec postgres rmbackup backup_2018_03_13T09_05_07.sql.gz diff --git a/requirements.txt b/requirements.txt index 198cf3ab27..67553f6876 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,18 +4,18 @@ binaryornot==0.4.4 # Code quality # ------------------------------------------------------------------------------ -black==23.9.1 +black==23.11.0 isort==5.12.0 flake8==6.1.0 django-upgrade==1.15.0 djlint==1.34.0 -pre-commit==3.4.0 +pre-commit==3.5.0 # Testing # ------------------------------------------------------------------------------ tox==4.11.3 -pytest==7.4.2 -pytest-xdist==3.3.1 +pytest==7.4.3 +pytest-xdist==3.4.0 pytest-cookies==0.7.0 pytest-instafail==0.5.0 pyyaml==6.0.1 @@ -23,6 +23,6 @@ pyyaml==6.0.1 # Scripting # ------------------------------------------------------------------------------ PyGithub==2.1.1 -gitpython==3.1.37 +gitpython==3.1.40 jinja2==3.1.2 requests==2.31.0 diff --git a/setup.py b/setup.py index a4bfbb7a9c..7245f9beb0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from distutils.core import setup # We use calendar versioning -version = "2023.10.09" +version = "2023.11.14" with open("README.md") as readme_file: long_description = readme_file.read() diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index e29068d58a..3601ebce46 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: detect-private-key - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v3.1.0 hooks: - id: prettier args: ['--tab-width', '2', '--single-quote'] @@ -37,7 +37,7 @@ repos: args: [--py311-plus] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.11.0 hooks: - id: black diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/rmbackup b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/rmbackup new file mode 100644 index 0000000000..fdfd20e148 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/rmbackup @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +### Remove a database backup. +### +### Parameters: +### <1> filename of a backup to remove. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres rmbackup <1> + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +if [[ -z ${1+x} ]]; then + message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." + exit 1 +fi +backup_filename="${BACKUP_DIR_PATH}/${1}" +if [[ ! -f "${backup_filename}" ]]; then + message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." + exit 1 +fi + +message_welcome "Removing the '${backup_filename}' backup file..." + +rm -r "${backup_filename}" + +message_success "The '${backup_filename}' database backup has been removed." diff --git a/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile index e547dfbb83..b85b02aa0c 100644 --- a/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile @@ -1,4 +1,4 @@ -FROM traefik:2.10.4 +FROM traefik:2.10.5 RUN mkdir -p /etc/traefik/acme \ && touch /etc/traefik/acme/acme.json \ && chmod 600 /etc/traefik/acme/acme.json diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index f862b12c2b..37be93d9f5 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -1,5 +1,5 @@ python-slugify==8.0.1 # https://github.com/un33k/python-slugify -Pillow==10.0.1 # https://github.com/python-pillow/Pillow +Pillow==10.1.0 # https://github.com/python-pillow/Pillow {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} {%- if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %} rcssmin==1.1.0 --install-option="--without-c-extensions" # https://github.com/ndparker/rcssmin @@ -9,31 +9,31 @@ rcssmin==1.1.1 # https://github.com/ndparker/rcssmin {%- endif %} argon2-cffi==23.1.0 # https://github.com/hynek/argon2_cffi {%- if cookiecutter.use_whitenoise == 'y' %} -whitenoise==6.5.0 # https://github.com/evansd/whitenoise +whitenoise==6.6.0 # https://github.com/evansd/whitenoise {%- endif %} redis==5.0.1 # https://github.com/redis/redis-py {%- if cookiecutter.use_docker == "y" or cookiecutter.windows == "n" %} hiredis==2.2.3 # https://github.com/redis/hiredis-py {%- endif %} {%- if cookiecutter.use_celery == "y" %} -celery==5.3.4 # pyup: < 6.0 # https://github.com/celery/celery +celery==5.3.5 # pyup: < 6.0 # https://github.com/celery/celery django-celery-beat==2.5.0 # https://github.com/celery/django-celery-beat {%- if cookiecutter.use_docker == 'y' %} flower==2.0.1 # https://github.com/mher/flower {%- endif %} {%- endif %} {%- if cookiecutter.use_async == 'y' %} -uvicorn[standard]==0.23.2 # https://github.com/encode/uvicorn +uvicorn[standard]==0.24.0.post1 # https://github.com/encode/uvicorn {%- endif %} # Django # ------------------------------------------------------------------------------ -django==4.2.6 # pyup: < 5.0 # https://www.djangoproject.com/ +django==4.2.7 # pyup: < 5.0 # https://www.djangoproject.com/ django-environ==0.11.2 # https://github.com/joke2k/django-environ django-model-utils==4.3.1 # https://github.com/jazzband/django-model-utils -django-allauth==0.57.0 # https://github.com/pennersr/django-allauth -django-crispy-forms==2.0 # https://github.com/django-crispy-forms/django-crispy-forms -crispy-bootstrap5==0.7 # https://github.com/django-crispy-forms/crispy-bootstrap5 +django-allauth==0.58.2 # https://github.com/pennersr/django-allauth +django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms +crispy-bootstrap5==2023.10 # https://github.com/django-crispy-forms/crispy-bootstrap5 {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} django-compressor==4.4 # https://github.com/django-compressor/django-compressor {%- endif %} @@ -41,7 +41,7 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis {%- if cookiecutter.use_drf == 'y' %} # Django REST Framework djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework -django-cors-headers==4.2.0 # https://github.com/adamchainz/django-cors-headers +django-cors-headers==4.3.1 # https://github.com/adamchainz/django-cors-headers # DRF-spectacular for api documentation drf-spectacular==0.26.5 # https://github.com/tfranzel/drf-spectacular {%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 9e4df64005..78ffc43d70 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -1,24 +1,24 @@ -r base.txt -Werkzeug[watchdog]==3.0.0 # https://github.com/pallets/werkzeug +Werkzeug[watchdog]==3.0.1 # https://github.com/pallets/werkzeug ipdb==0.13.13 # https://github.com/gotcha/ipdb {%- if cookiecutter.use_docker == 'y' %} -psycopg[c]==3.1.12 # https://github.com/psycopg/psycopg +psycopg[c]==3.1.13 # https://github.com/psycopg/psycopg {%- else %} -psycopg[binary]==3.1.12 # https://github.com/psycopg/psycopg +psycopg[binary]==3.1.13 # https://github.com/psycopg/psycopg {%- endif %} {%- if cookiecutter.use_async == 'y' or cookiecutter.use_celery == 'y' %} -watchfiles==0.20.0 # https://github.com/samuelcolvin/watchfiles +watchfiles==0.21.0 # https://github.com/samuelcolvin/watchfiles {%- endif %} # Testing # ------------------------------------------------------------------------------ -mypy==1.5.1 # https://github.com/python/mypy -django-stubs[compatible-mypy]==4.2.4 # https://github.com/typeddjango/django-stubs -pytest==7.4.2 # https://github.com/pytest-dev/pytest +mypy==1.6.1 # https://github.com/python/mypy +django-stubs[compatible-mypy]==4.2.6 # https://github.com/typeddjango/django-stubs +pytest==7.4.3 # https://github.com/pytest-dev/pytest pytest-sugar==0.9.7 # https://github.com/Frozenball/pytest-sugar {%- if cookiecutter.use_drf == "y" %} -djangorestframework-stubs[compatible-mypy]==3.14.3 # https://github.com/typeddjango/djangorestframework-stubs +djangorestframework-stubs[compatible-mypy]==3.14.4 # https://github.com/typeddjango/djangorestframework-stubs {%- endif %} # Documentation @@ -29,15 +29,15 @@ sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild # Code quality # ------------------------------------------------------------------------------ flake8==6.1.0 # https://github.com/PyCQA/flake8 -flake8-isort==6.1.0 # https://github.com/gforcada/flake8-isort +flake8-isort==6.1.1 # https://github.com/gforcada/flake8-isort coverage==7.3.2 # https://github.com/nedbat/coveragepy -black==23.9.1 # https://github.com/psf/black +black==23.11.0 # https://github.com/psf/black djlint==1.34.0 # https://github.com/Riverside-Healthcare/djLint -pylint-django==2.5.3 # https://github.com/PyCQA/pylint-django +pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django {%- if cookiecutter.use_celery == 'y' %} pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery {%- endif %} -pre-commit==3.4.0 # https://github.com/pre-commit/pre-commit +pre-commit==3.5.0 # https://github.com/pre-commit/pre-commit # Django # ------------------------------------------------------------------------------ @@ -46,4 +46,4 @@ factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy django-debug-toolbar==4.2.0 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.2.3 # https://github.com/django-extensions/django-extensions django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin -pytest-django==4.5.2 # https://github.com/pytest-dev/pytest-django +pytest-django==4.7.0 # https://github.com/pytest-dev/pytest-django diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index e2375b2221..2a17f253e3 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -3,12 +3,12 @@ -r base.txt gunicorn==21.2.0 # https://github.com/benoitc/gunicorn -psycopg[c]==3.1.12 # https://github.com/psycopg/psycopg +psycopg[c]==3.1.13 # https://github.com/psycopg/psycopg {%- if cookiecutter.use_whitenoise == 'n' %} Collectfast==2.2.0 # https://github.com/antonagestam/collectfast {%- endif %} {%- if cookiecutter.use_sentry == "y" %} -sentry-sdk==1.31.0 # https://github.com/getsentry/sentry-python +sentry-sdk==1.35.0 # https://github.com/getsentry/sentry-python {%- endif %} {%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %} hiredis==2.2.3 # https://github.com/redis/hiredis-py @@ -24,21 +24,21 @@ django-storages[google]==1.14.2 # https://github.com/jschneier/django-storages django-storages[azure]==1.14.2 # https://github.com/jschneier/django-storages {%- endif %} {%- if cookiecutter.mail_service == 'Mailgun' %} -django-anymail[mailgun]==10.1 # https://github.com/anymail/django-anymail +django-anymail[mailgun]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'Amazon SES' %} -django-anymail[amazon-ses]==10.1 # https://github.com/anymail/django-anymail +django-anymail[amazon-ses]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'Mailjet' %} -django-anymail[mailjet]==10.1 # https://github.com/anymail/django-anymail +django-anymail[mailjet]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'Mandrill' %} -django-anymail[mandrill]==10.1 # https://github.com/anymail/django-anymail +django-anymail[mandrill]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'Postmark' %} -django-anymail[postmark]==10.1 # https://github.com/anymail/django-anymail +django-anymail[postmark]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'Sendgrid' %} -django-anymail[sendgrid]==10.1 # https://github.com/anymail/django-anymail +django-anymail[sendgrid]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'SendinBlue' %} -django-anymail[sendinblue]==10.1 # https://github.com/anymail/django-anymail +django-anymail[sendinblue]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'SparkPost' %} -django-anymail[sparkpost]==10.1 # https://github.com/anymail/django-anymail +django-anymail[sparkpost]==10.2 # https://github.com/anymail/django-anymail {%- elif cookiecutter.mail_service == 'Other SMTP' %} -django-anymail==10.1 # https://github.com/anymail/django-anymail +django-anymail==10.2 # https://github.com/anymail/django-anymail {%- endif %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py index c5c824bda7..874ad61d90 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py @@ -27,11 +27,12 @@ def populate_user(self, request: HttpRequest, sociallogin: SocialLogin, data: di See: https://django-allauth.readthedocs.io/en/latest/advanced.html?#creating-and-populating-user-instances """ - user = sociallogin.user - if name := data.get("name"): - user.name = name - elif first_name := data.get("first_name"): - user.name = first_name - if last_name := data.get("last_name"): - user.name += f" {last_name}" - return super().populate_user(request, sociallogin, data) + user = super().populate_user(request, sociallogin, data) + if not user.name: + if name := data.get("name"): + user.name = name + elif first_name := data.get("first_name"): + user.name = first_name + if last_name := data.get("last_name"): + user.name += f" {last_name}" + return user