From b1b5ef072f0d8d10cdc4fe9e39a9a753db8103cb Mon Sep 17 00:00:00 2001 From: Gary Snider <75227981+gsnider2195@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:38:42 -0500 Subject: [PATCH 1/9] Fix url typo in design_development.md (#175) --- docs/user/design_development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/design_development.md b/docs/user/design_development.md index cd820e36..450d579f 100644 --- a/docs/user/design_development.md +++ b/docs/user/design_development.md @@ -14,7 +14,7 @@ For the remainder of this tutorial we will focus solely on the Design Job, Desig ## Design Components -Designs can be loaded either from local files or from a git repository. Either way, the structure of the actual designs and all the associated files is the same. Since, fundamentally, all designs are Nautobot Jobs, everything must be in a top level `jobs` python package (meaning the directory must contain the file `__init__.py`) and all design classes must be either defined in this `jobs` module or be imported to it. The following directory layout is from the [demo designs repository](hhttps://github.com/nautobot/demo-designs): +Designs can be loaded either from local files or from a git repository. Either way, the structure of the actual designs and all the associated files is the same. Since, fundamentally, all designs are Nautobot Jobs, everything must be in a top level `jobs` python package (meaning the directory must contain the file `__init__.py`) and all design classes must be either defined in this `jobs` module or be imported to it. The following directory layout is from the [demo designs repository](https://github.com/nautobot/demo-designs): ``` bash jobs From 150db65731cfa1c3fdc43895f2972bb68a3e2804 Mon Sep 17 00:00:00 2001 From: ehendrickson2 <134078930+ehendrickson2@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:37:22 -0500 Subject: [PATCH 2/9] Fix typo in function name in design_development.md (#177) Co-authored-by: Eddie Hendrickson --- docs/user/design_development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/design_development.md b/docs/user/design_development.md index 450d579f..0be2a36b 100644 --- a/docs/user/design_development.md +++ b/docs/user/design_development.md @@ -49,7 +49,7 @@ The `jobs` directory contains everything that is needed for implementing a desig Within the `jobs` directory, the naming of modules and files is not important. However, it is recommended to use intuitive names to help understand each file's relationship with others. For instance, above there is are design contexts specified as both Python modules as well as YAML files and each design has exactly one design template. The relationship of context YAML files and context Python modules will be discussed later. -Designs are just specialized Nautobot jobs. Any design must inherit from `DesignJob`, and just like any other job, design jobs must be registered using `register_jbos`. An example design follows: +Designs are just specialized Nautobot jobs. Any design must inherit from `DesignJob`, and just like any other job, design jobs must be registered using `register_jobs`. An example design follows: ```python from nautobot.apps.jobs import register_jobs From 87dcba79a0bd123e8803134c0cf334c93a78a162 Mon Sep 17 00:00:00 2001 From: Eddie Hendrickson Date: Thu, 11 Jul 2024 10:20:48 -0500 Subject: [PATCH 3/9] added release notes for v2.0 to nav --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 38942f2b..5a0d912e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,6 +118,7 @@ nav: - "admin/release_notes/index.md" - v1.0: "admin/release_notes/version_1.0.md" - v1.1: "admin/release_notes/version_1.1.md" + - v2.0: "admin/release_notes/version_2.0.md" - Developer Guide: - Extending the App: "dev/extending.md" - Contributing to the App: "dev/contributing.md" From 7489492dd41b73e23584664d9cf81816bf41a648 Mon Sep 17 00:00:00 2001 From: Ken Celenza Date: Sun, 8 Sep 2024 11:37:03 -0400 Subject: [PATCH 4/9] Minor doc update to add get as an action. --- docs/dev/extending.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/extending.md b/docs/dev/extending.md index a9952735..f75ebf84 100644 --- a/docs/dev/extending.md +++ b/docs/dev/extending.md @@ -5,7 +5,7 @@ Design builder is primarily extended by creating new action tags. These action t ## Action Tag Extensions The action tags in Design Builder are provided by `design.Builder`. This component reads a design and then executes instructions that are specified in the design. Basic functions, provided out of the box, are -`create`, `create_or_update` and `update`. These actions are self explanatory (for details on syntax see [this document](../user//design_development.md#special-syntax)). Two additional actions are provided, these are the `ref` and `git_context` actions. These two actions are provided as extensions to the builder. +`get`, `create`, `create_or_update` and `update`. These actions are self explanatory (for details on syntax see [this document](../user//design_development.md#special-syntax)). Two additional actions are provided, these are the `ref` and `git_context` actions. These two actions are provided as extensions to the builder. Extensions specify attribute and/or value actions to the object creator. Within a design template, these extensions can be used by specifying an exclamation point (!) followed by the extensions attribute or value tag. For instance, the `ref` extension implements both an attribute and a value extension. This extension can be used by specifying `!ref`. Extensions can add behavior to the object creator that is not supplied by the standard create and update actions. From 764481664016be21df6928e3d7039d31daf28703 Mon Sep 17 00:00:00 2001 From: Andrew Bates Date: Thu, 19 Sep 2024 10:52:53 -0400 Subject: [PATCH 5/9] Updated upper bound for Python version --- poetry.lock | 462 +++++++++++++++++++++++++++++++++++-------------- pyproject.toml | 7 +- 2 files changed, 335 insertions(+), 134 deletions(-) diff --git a/poetry.lock b/poetry.lock index 752b7b76..8fa50ba3 100755 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "amqp" @@ -55,17 +55,22 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "astroid" -version = "3.1.0" +version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.7.2" files = [ - {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, - {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, + {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, + {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, ] [package.dependencies] +lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +wrapt = [ + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +] [[package]] name = "asttokens" @@ -743,19 +748,20 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "django" -version = "3.2.25" -description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +version = "4.2.16" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38"}, - {file = "Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777"}, + {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, + {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, ] [package.dependencies] -asgiref = ">=3.3.2,<4" -pytz = "*" -sqlparse = ">=0.2.2" +asgiref = ">=3.6.0,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] @@ -808,36 +814,35 @@ Django = ">=3.2.18" [[package]] name = "django-constance" -version = "2.9.1" +version = "3.1.0" description = "Django live settings with pluggable backends, including Redis." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "django-constance-2.9.1.tar.gz", hash = "sha256:4c6a96a5f2cbce1bc3fa41aa20566b6ee26fbd896c9f91f996518a3a0904f6c8"}, - {file = "django_constance-2.9.1-py3-none-any.whl", hash = "sha256:bf0b392efa18a1f3f464eddb7eb36ac5c02598354a5e31d0d4ce4fc8b535694b"}, + {file = "django-constance-3.1.0.tar.gz", hash = "sha256:2b96e51de63751ef63f8f92f74e0f6aea30fb6453f3a736c21e1f8b3f6cf0b4f"}, + {file = "django_constance-3.1.0-py3-none-any.whl", hash = "sha256:6242486a346e396d765a9333d17f3101c8613cabc92e0b98dcb70c2a391bc53b"}, ] [package.dependencies] -django-picklefield = {version = "*", optional = true, markers = "extra == \"database\""} +django-picklefield = "*" [package.extras] -database = ["django-picklefield"] redis = ["redis"] [[package]] name = "django-cors-headers" -version = "4.3.1" +version = "4.4.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" files = [ - {file = "django-cors-headers-4.3.1.tar.gz", hash = "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207"}, - {file = "django_cors_headers-4.3.1-py3-none-any.whl", hash = "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36"}, + {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, + {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, ] [package.dependencies] asgiref = ">=3.6" -Django = ">=3.2" +django = ">=3.2" [[package]] name = "django-db-file-storage" @@ -884,27 +889,27 @@ Django = ">=3.2" [[package]] name = "django-filter" -version = "23.5" +version = "24.2" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "django-filter-23.5.tar.gz", hash = "sha256:67583aa43b91fe8c49f74a832d95f4d8442be628fd4c6d65e9f811f5153a4e5c"}, - {file = "django_filter-23.5-py3-none-any.whl", hash = "sha256:99122a201d83860aef4fe77758b69dda913e874cc5e0eaa50a86b0b18d708400"}, + {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, + {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, ] [package.dependencies] -Django = ">=3.2" +Django = ">=4.2" [[package]] name = "django-health-check" -version = "3.18.1" +version = "3.18.3" description = "Run checks on services like databases, queue servers, celery processes, etc." optional = false python-versions = ">=3.8" files = [ - {file = "django-health-check-3.18.1.tar.gz", hash = "sha256:44552d55ae8950c9548d3b90f9d9fd5570b57446a19b2a8e674c82f993cb7a2c"}, - {file = "django_health_check-3.18.1-py2.py3-none-any.whl", hash = "sha256:2c89a326cd79830e2fc6808823a9e7e874ab23f7aef3ff2c4d1194c998e1dca1"}, + {file = "django_health_check-3.18.3-py2.py3-none-any.whl", hash = "sha256:f5f58762b80bdf7b12fad724761993d6e83540f97e2c95c42978f187e452fa07"}, + {file = "django_health_check-3.18.3.tar.gz", hash = "sha256:18b75daca4551c69a43f804f9e41e23f5f5fb9efd06cf6a313b3d5031bb87bd0"}, ] [package.dependencies] @@ -912,7 +917,21 @@ django = ">=2.2" [package.extras] docs = ["sphinx"] -test = ["celery", "pytest", "pytest-cov", "pytest-django", "redis"] +test = ["boto3", "celery", "django-storages", "pytest", "pytest-cov", "pytest-django", "redis"] + +[[package]] +name = "django-ipware" +version = "7.0.1" +description = "A Django application to retrieve user's IP address" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-ipware-7.0.1.tar.gz", hash = "sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47"}, + {file = "django_ipware-7.0.1-py2.py3-none-any.whl", hash = "sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709"}, +] + +[package.dependencies] +python-ipware = ">=2.0.3" [[package]] name = "django-jinja" @@ -995,6 +1014,27 @@ Django = ">=3.2" gprof2dot = ">=2017.09.19" sqlparse = "*" +[[package]] +name = "django-structlog" +version = "8.1.0" +description = "Structured Logging for Django" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_structlog-8.1.0-py3-none-any.whl", hash = "sha256:1072564bd6f36e8d3ba9893e7b31c1c46e94301189fedaecc0fb8a46525a3214"}, + {file = "django_structlog-8.1.0.tar.gz", hash = "sha256:0229b9a2efbd24a4e3500169788e53915c2429521e34e41dd58ccc56039bef3f"}, +] + +[package.dependencies] +asgiref = ">=3.6.0" +django = ">=4.2" +django-ipware = ">=6.0.2" +structlog = ">=21.4.0" + +[package.extras] +celery = ["celery (>=5.1)"] +commands = ["django-extensions (>=1.4.9)"] + [[package]] name = "django-tables2" version = "2.7.0" @@ -1014,43 +1054,42 @@ tablib = ["tablib"] [[package]] name = "django-taggit" -version = "4.0.0" +version = "5.0.1" description = "django-taggit is a reusable Django application for simple tagging." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "django-taggit-4.0.0.tar.gz", hash = "sha256:4d52de9d37245a9b9f98c0ec71fdccf1d2283e38e8866d40a7ae6a3b6787a161"}, - {file = "django_taggit-4.0.0-py3-none-any.whl", hash = "sha256:eb800dabef5f0a4e047ab0751f82cf805bc4a9e972037ef12bf519f52cd92480"}, + {file = "django-taggit-5.0.1.tar.gz", hash = "sha256:edcd7db1e0f35c304e082a2f631ddac2e16ef5296029524eb792af7430cab4cc"}, + {file = "django_taggit-5.0.1-py3-none-any.whl", hash = "sha256:a0ca8a28b03c4b26c2630fd762cb76ec39b5e41abf727a7b66f897a625c5e647"}, ] [package.dependencies] -Django = ">=3.2" +Django = ">=4.1" [[package]] name = "django-timezone-field" -version = "5.1" +version = "7.0" description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." optional = false -python-versions = ">=3.7,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "django_timezone_field-5.1-py3-none-any.whl", hash = "sha256:16ca9955a4e16064e32168b1a0d1cdb2839679c6cb56856c1f49f506e2ca4281"}, - {file = "django_timezone_field-5.1.tar.gz", hash = "sha256:73fc49519273cd5da1c7f16abc04a4bcad87b00cc02968d0d384c0fecf9a8a86"}, + {file = "django_timezone_field-7.0-py3-none-any.whl", hash = "sha256:3232e7ecde66ba4464abb6f9e6b8cc739b914efb9b29dc2cf2eee451f7cc2acb"}, + {file = "django_timezone_field-7.0.tar.gz", hash = "sha256:aa6f4965838484317b7f08d22c0d91a53d64e7bbbd34264468ae83d4023898a7"}, ] [package.dependencies] "backports.zoneinfo" = {version = ">=0.2.1,<0.3.0", markers = "python_version < \"3.9\""} -Django = ">=2.2,<3.0.dev0 || >=3.2.dev0,<5.0" -pytz = "*" +Django = ">=3.2,<6.0" [[package]] name = "django-tree-queries" -version = "0.17.0" +version = "0.19.0" description = "Tree queries with explicit opt-in, without configurability" optional = false python-versions = ">=3.8" files = [ - {file = "django_tree_queries-0.17.0-py3-none-any.whl", hash = "sha256:df62cc7daa7a766483a8ae11618ff7649d74425b5d245e9644526f2dd2f51af0"}, - {file = "django_tree_queries-0.17.0.tar.gz", hash = "sha256:f115cf6756c55fde56bb876d5b5aa1b2bd33ae3d6e2949c3176ef0b4fb64c532"}, + {file = "django_tree_queries-0.19.0-py3-none-any.whl", hash = "sha256:05b9e3158e31612528f136b4704a8d807e14edc0b4a607a45377e6132517ba2c"}, + {file = "django_tree_queries-0.19.0.tar.gz", hash = "sha256:d1325e75f96e90b86c4316a3d63498101ec05703f4e629786b561e8aaab0e4a7"}, ] [package.extras] @@ -1079,18 +1118,18 @@ waitress = ["waitress"] [[package]] name = "djangorestframework" -version = "3.15.1" +version = "3.15.2" description = "Web APIs for Django, made easy." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, - {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, + {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, + {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, ] [package.dependencies] "backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} -django = ">=3.0" +django = ">=4.2" [[package]] name = "drf-react-template-framework" @@ -1108,13 +1147,13 @@ djangorestframework = ">=3.12.0,<4.0.0" [[package]] name = "drf-spectacular" -version = "0.26.5" +version = "0.27.2" description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "drf-spectacular-0.26.5.tar.gz", hash = "sha256:aee55330a774ba8a9cbdb125714d1c9ee05a8aafd3ce3be8bfd26527649aeb44"}, - {file = "drf_spectacular-0.26.5-py3-none-any.whl", hash = "sha256:c0002a820b11771fdbf37853deb371947caf0159d1afeeffe7598e964bc1db94"}, + {file = "drf-spectacular-0.27.2.tar.gz", hash = "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981"}, + {file = "drf_spectacular-0.27.2-py3-none-any.whl", hash = "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b"}, ] [package.dependencies] @@ -1124,6 +1163,7 @@ drf-spectacular-sidecar = {version = "*", optional = true, markers = "extra == \ inflection = ">=0.3.1" jsonschema = ">=2.6.0" PyYAML = ">=5.1" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} uritemplate = ">=2.0.0" [package.extras] @@ -1146,17 +1186,20 @@ Django = ">=2.2" [[package]] name = "emoji" -version = "2.11.0" +version = "2.12.1" description = "Emoji for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "emoji-2.11.0-py2.py3-none-any.whl", hash = "sha256:63fc9107f06c6c2e48e5078ce9575cef98518f5ac09474f6148a43e989989582"}, - {file = "emoji-2.11.0.tar.gz", hash = "sha256:772eaa30f4e0b1ce95148a092df4c7dc97644532c03225326b0fd05e8a9f72a3"}, + {file = "emoji-2.12.1-py3-none-any.whl", hash = "sha256:a00d62173bdadc2510967a381810101624a2f0986145b8da0cffa42e29430235"}, + {file = "emoji-2.12.1.tar.gz", hash = "sha256:4aa0488817691aa58d83764b6c209f8a27c0b3ab3f89d1b8dceca1a62e4973eb"}, ] +[package.dependencies] +typing-extensions = ">=4.7.0" + [package.extras] -dev = ["coverage", "coveralls", "pytest"] +dev = ["coverage", "pytest (>=7.4.4)"] [[package]] name = "executing" @@ -1499,13 +1542,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1586,15 +1629,61 @@ sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] +[[package]] +name = "lazy-object-proxy" +version = "1.10.0" +description = "A fast and thorough lazy object proxy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, + {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, +] + [[package]] name = "markdown" -version = "3.5.2" +version = "3.6" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, - {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] [package.dependencies] @@ -1889,48 +1978,49 @@ files = [ [[package]] name = "nautobot" -version = "2.2.1" +version = "2.3.2" description = "Source of truth and network automation platform." optional = false -python-versions = "<3.12,>=3.8" +python-versions = "<3.13,>=3.8" files = [ - {file = "nautobot-2.2.1-py3-none-any.whl", hash = "sha256:c65bfa5ef5abc32d70f7dbea0f821f8f865ede0135cb36398029c4b96ed42875"}, - {file = "nautobot-2.2.1.tar.gz", hash = "sha256:bbf9f6cffe0f4aa064b5e0a470028c8bffe6be0605dba49be70a849a2ed47d57"}, + {file = "nautobot-2.3.2-py3-none-any.whl", hash = "sha256:5318a26af1dde8919345bd242a3ed2be221bf2cc11149708fdcfdc55470b761a"}, + {file = "nautobot-2.3.2.tar.gz", hash = "sha256:03f0c7ca0224bf2a37a0a81ef978a20284c44e896a14e75bd403a0d09c2f913d"}, ] [package.dependencies] -celery = ">=5.3.1,<5.4.0" -Django = ">=3.2.25,<3.3.0" +celery = ">=5.3.6,<5.4.0" +Django = ">=4.2.15,<4.3.0" django-ajax-tables = ">=1.1.1,<1.2.0" django-celery-beat = ">=2.6.0,<2.7.0" django-celery-results = ">=2.5.1,<2.6.0" -django-constance = {version = ">=2.9.1,<2.10.0", extras = ["database"]} -django-cors-headers = ">=4.3.1,<4.4.0" -django-db-file-storage = ">=0.5.5,<0.6.0" +django-constance = ">=3.1.0,<3.2.0" +django-cors-headers = ">=4.4.0,<4.5.0" +django-db-file-storage = ">=0.5.6.1,<0.6.0.0" django-extensions = ">=3.2.3,<3.3.0" -django-filter = ">=23.5,<23.6" -django-health-check = ">=3.18.1,<3.19.0" +django-filter = ">=24.2,<24.3" +django-health-check = ">=3.18.3,<3.19.0" django-jinja = ">=2.11.0,<2.12.0" django-prometheus = ">=2.3.1,<2.4.0" django-redis = ">=5.4.0,<5.5.0" django-silk = ">=5.1.0,<5.2.0" +django-structlog = {version = ">=8.1.0,<9.0.0", extras = ["all"]} django-tables2 = ">=2.7.0,<2.8.0" -django-taggit = ">=4.0.0,<4.1.0" -django-timezone-field = ">=5.1,<5.2" -django-tree-queries = ">=0.17.0,<0.18.0" +django-taggit = ">=5.0.0,<5.1.0" +django-timezone-field = ">=7.0,<7.1" +django-tree-queries = ">=0.19.0,<0.20.0" django-webserver = ">=1.2.0,<1.3.0" -djangorestframework = ">=3.15.1,<3.16.0" +djangorestframework = ">=3.15.2,<3.16.0" drf-react-template-framework = ">=0.0.17,<0.0.18" -drf-spectacular = {version = ">=0.26.5,<0.27.0", extras = ["sidecar"]} -emoji = ">=2.11.0,<2.12.0" +drf-spectacular = {version = ">=0.27.2,<0.28.0", extras = ["sidecar"]} +emoji = ">=2.12.1,<2.13.0" GitPython = ">=3.1.43,<3.2.0" graphene-django = ">=2.16.0,<2.17.0" graphene-django-optimizer = ">=0.8.0,<0.9.0" -Jinja2 = ">=3.1.3,<3.2.0" +Jinja2 = ">=3.1.4,<3.2.0" jsonschema = ">=4.7.0,<5.0.0" -Markdown = ">=3.5.2,<3.6.0" +Markdown = ">=3.6,<3.7" MarkupSafe = ">=2.1.5,<2.2.0" -netaddr = ">=0.10.1,<0.11.0" +netaddr = ">=1.3.0,<1.4.0" netutils = ">=1.6.0,<2.0.0" nh3 = ">=0.2.15,<0.3.0" packaging = ">=23.1" @@ -1940,43 +2030,54 @@ psycopg2-binary = ">=2.9.9,<2.10.0" python-slugify = ">=8.0.3,<8.1.0" pyuwsgi = ">=2.0.23,<2.1.0" PyYAML = ">=6.0,<6.1" -social-auth-app-django = ">=5.4.0,<5.5.0" +social-auth-app-django = ">=5.4.2,<5.5.0" svgwrite = ">=1.4.2,<1.5.0" [package.extras] -all = ["django-auth-ldap (>=4.7.0,<4.8.0)", "django-storages (>=1.14.2,<1.15.0)", "mysqlclient (>=2.2.3,<2.3.0)", "napalm (>=4.1.0,<4.2.0)", "social-auth-core[openidconnect,saml] (>=4.5.3,<4.6.0)"] -ldap = ["django-auth-ldap (>=4.7.0,<4.8.0)"] +all = ["django-auth-ldap (>=4.8.0,<4.9.0)", "django-storages (==1.14.3)", "mysqlclient (>=2.2.3,<2.3.0)", "napalm (>=4.1.0,<6.0.0)", "social-auth-core[saml] (>=4.5.3,<4.6.0)"] +ldap = ["django-auth-ldap (>=4.8.0,<4.9.0)"] mysql = ["mysqlclient (>=2.2.3,<2.3.0)"] -napalm = ["napalm (>=4.1.0,<4.2.0)"] -remote-storage = ["django-storages (>=1.14.2,<1.15.0)"] -sso = ["social-auth-core[openidconnect,saml] (>=4.5.3,<4.6.0)"] +napalm = ["napalm (>=4.1.0,<6.0.0)"] +remote-storage = ["django-storages (==1.14.3)"] +sso = ["social-auth-core[saml] (>=4.5.3,<4.6.0)"] [[package]] name = "nautobot-bgp-models" -version = "2.0.0" +version = "2.2.1b1" description = "Nautobot BGP Models App" optional = false -python-versions = ">=3.8,<3.12" -files = [ - {file = "nautobot_bgp_models-2.0.0-py3-none-any.whl", hash = "sha256:2d8ac457a29ec6cf0d7bf99320ddddb8f0232302e5d09b044e5708b9c6824c8c"}, - {file = "nautobot_bgp_models-2.0.0.tar.gz", hash = "sha256:97dc0b3179a5548c05a8ea20ee46e2c0e5a2fb7218c66a4ff8c609c374ef9199"}, -] +python-versions = ">=3.8,<3.13" +files = [] +develop = false [package.dependencies] -nautobot = ">=2.0.3,<3.0.0" -toml = ">=0.10.2,<0.11.0" +nautobot = "^2.0.3" +netutils = "^1.6.0" +toml = "^0.10.2" + +[package.extras] +all = [] + +[package.source] +type = "git" +url = "https://github.com/nautobot/nautobot-app-bgp-models.git" +reference = "develop" +resolved_reference = "426388c871adae48eb5a99c037603b59d0c8e3c5" [[package]] name = "netaddr" -version = "0.10.1" +version = "1.3.0" description = "A network address manipulation library for Python" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "netaddr-0.10.1-py2.py3-none-any.whl", hash = "sha256:9822305b42ea1020d54fee322d43cee5622b044c07a1f0130b459bb467efcf88"}, - {file = "netaddr-0.10.1.tar.gz", hash = "sha256:f4da4222ca8c3f43c8e18a8263e5426c750a3a837fdfeccf74c68d0408eaa3bf"}, + {file = "netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe"}, + {file = "netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a"}, ] +[package.extras] +nicer-shell = ["ipython"] + [[package]] name = "netutils" version = "1.8.0" @@ -2448,23 +2549,23 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "3.1.0" +version = "2.17.7" description = "python code static checker" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.7.2" files = [ - {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, - {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, + {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, + {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, ] [package.dependencies] -astroid = ">=3.1.0,<=3.2.0-dev0" +astroid = ">=2.15.8,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, ] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -2495,18 +2596,18 @@ with-django = ["Django (>=2.2)"] [[package]] name = "pylint-nautobot" -version = "0.3.0" +version = "0.3.1" description = "Custom Pylint Rules for Nautobot" optional = false -python-versions = ">=3.8,<3.12" +python-versions = "<4.0,>=3.8" files = [ - {file = "pylint_nautobot-0.3.0-py3-none-any.whl", hash = "sha256:91fed48d9a9f565c6aa46c679b930d64b06d014061f6e7e802e6de8b6b8e3f87"}, - {file = "pylint_nautobot-0.3.0.tar.gz", hash = "sha256:387a1d73b49186a7b325b6c1a3634e2c57ec0f2350e93494304c47073400099b"}, + {file = "pylint_nautobot-0.3.1-py3-none-any.whl", hash = "sha256:097bb85405aabe766395a9d09dc474e39c8d9d23700d0e64e21f3855b4188466"}, + {file = "pylint_nautobot-0.3.1.tar.gz", hash = "sha256:3a637f03dccf29db47d6fdfa348f6fbcd97e9471ade5c8eca7efd0921740dd94"}, ] [package.dependencies] importlib-resources = ">=5.12.0" -pylint = ">=2.17.5" +pylint = ">=2.17,<3.0" pyyaml = ">=6.0.1" toml = ">=0.10.2" @@ -2574,6 +2675,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-ipware" +version = "3.0.0" +description = "A Python package to retrieve user's IP address" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python_ipware-3.0.0-py3-none-any.whl", hash = "sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60"}, + {file = "python_ipware-3.0.0.tar.gz", hash = "sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062"}, +] + +[package.extras] +dev = ["coverage[toml]", "coveralls (>=3.3,<4.0)", "ruff", "twine"] + [[package]] name = "python-slugify" version = "8.0.4" @@ -2609,17 +2724,6 @@ defusedxml = "*" mysql = ["mysql-connector-python"] postgresql = ["psycopg2"] -[[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - [[package]] name = "pyuwsgi" version = "2.0.23.post0" @@ -3110,13 +3214,13 @@ files = [ [[package]] name = "social-auth-app-django" -version = "5.4.0" +version = "5.4.2" description = "Python Social Authentication, Django integration." optional = false python-versions = ">=3.8" files = [ - {file = "social-auth-app-django-5.4.0.tar.gz", hash = "sha256:09ac02a063cb313eed5e9ef2f9ac4477c8bf5bbd685925ff3aba43f9072f1bbb"}, - {file = "social_auth_app_django-5.4.0-py3-none-any.whl", hash = "sha256:28c65b2e2092f30cdb3cf912eeaa6988b49fdf4001b29bd89e683673d700a38e"}, + {file = "social-auth-app-django-5.4.2.tar.gz", hash = "sha256:c8832c6cf13da6ad76f5613bcda2647d89ae7cfbc5217fadd13477a3406feaa8"}, + {file = "social_auth_app_django-5.4.2-py3-none-any.whl", hash = "sha256:0c041a31707921aef9a930f143183c65d8c7b364381364a50f3f7c6fcc9d62f6"}, ] [package.dependencies] @@ -3197,6 +3301,23 @@ files = [ [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" +[[package]] +name = "structlog" +version = "24.4.0" +description = "Structured Logging for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610"}, + {file = "structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"}, +] + +[package.extras] +dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + [[package]] name = "svgwrite" version = "1.4.3" @@ -3414,6 +3535,85 @@ files = [ [package.extras] test = ["pytest (>=6.0.0)", "setuptools (>=65)"] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "yamllint" version = "1.35.1" @@ -3452,5 +3652,5 @@ nautobot = ["nautobot"] [metadata] lock-version = "2.0" -python-versions = ">=3.8.1,<3.12" -content-hash = "e5341722f9f0ffce4389c51a6cf88f4222d83e43312dc1560597c7de8fd36edd" +python-versions = ">=3.8.1,<3.13" +content-hash = "11a52050f39aff69dc4ce6adc5395eb5944028fdb7bb17ddc2bbab82d546f08f" diff --git a/pyproject.toml b/pyproject.toml index e58ed965..6fa2ce9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautobot-design-builder" -version = "2.0.0" +version = "2.0.1" description = "Nautobot app that uses design templates to easily create data objects in Nautobot with minimal input from a user." authors = ["Network to Code, LLC "] license = "Apache-2.0" @@ -26,7 +26,7 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8.1,<3.12" +python = ">=3.8.1,<3.13" # Used for local development nautobot = ">=2.0.3,<=2.9999" @@ -38,7 +38,7 @@ django-debug-toolbar = "*" flake8 = "*" invoke = "*" ipython = "*" -nautobot-bgp-models = "*" + pydocstyle = "*" pylint = "*" pylint-django = "*" @@ -58,6 +58,7 @@ mkdocstrings = "0.22.0" mkdocstrings-python = "1.5.2" gitpython = "^3.1.41" snakeviz = "^2.2.0" +nautobot-bgp-models = {git = "https://github.com/nautobot/nautobot-app-bgp-models.git", rev = "develop"} [tool.poetry.extras] nautobot = ["nautobot"] From 8c517b9ac15df429fc73eed6a2c4cae281713a19 Mon Sep 17 00:00:00 2001 From: Andrew Bates Date: Thu, 19 Sep 2024 11:02:02 -0400 Subject: [PATCH 6/9] Fixing linter error --- nautobot_design_builder/fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nautobot_design_builder/fields.py b/nautobot_design_builder/fields.py index 77d6bd8c..e0bb1f10 100644 --- a/nautobot_design_builder/fields.py +++ b/nautobot_design_builder/fields.py @@ -227,9 +227,9 @@ def _init_through(self): if self.field.remote_field.through_fields: self.link_field = self.field.remote_field.through_fields[0] else: - for f in self.through._meta.fields: - if f.related_model == self.field.model: - self.link_field = f.name + for field in self.through._meta.fields: + if field.related_model == self.field.model: + self.link_field = field.name def _get_related_model(self, value): """Get the appropriate related model for the value. @@ -295,9 +295,9 @@ def _init_through(self): if self.field.through_fields: self.link_field = self.field.through_fields[0] else: - for f in self.through._meta.fields: - if f.related_model == self.field.model: - self.link_field = f.name + for field in self.through._meta.fields: + if field.related_model == self.field.model: + self.link_field = field.name class GenericRelationField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods From d8d1f5fe3ef8ef0173af6969f74868057b08f753 Mon Sep 17 00:00:00 2001 From: Andrew Bates Date: Fri, 27 Sep 2024 09:32:00 -0400 Subject: [PATCH 7/9] Fixed custom field assignment --- nautobot_design_builder/design.py | 4 +- nautobot_design_builder/tests/test_builder.py | 29 +++++++++- .../tests/testdata/custom_fields.yaml | 54 +++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 nautobot_design_builder/tests/testdata/custom_fields.yaml diff --git a/nautobot_design_builder/design.py b/nautobot_design_builder/design.py index e339b9de..36a7c3d1 100644 --- a/nautobot_design_builder/design.py +++ b/nautobot_design_builder/design.py @@ -580,8 +580,8 @@ def _update_fields(self): elif hasattr(self.instance, field_name): setattr(self.instance, field_name, value) - for key, value in self.metadata.custom_fields.items(): - self.set_custom_field(key, value) + for key, value in self.metadata.custom_fields.items(): + self.set_custom_field(key, value) def save(self): """Save the model instance to the database. diff --git a/nautobot_design_builder/tests/test_builder.py b/nautobot_design_builder/tests/test_builder.py index b979715f..539e5f53 100644 --- a/nautobot_design_builder/tests/test_builder.py +++ b/nautobot_design_builder/tests/test_builder.py @@ -1,7 +1,6 @@ """Test object creator methods.""" import importlib -from operator import attrgetter import os from unittest.mock import patch import yaml @@ -83,6 +82,34 @@ def check_not_in(test, check, index): test.assertNotIn(value0, value1, msg=f"Check {index}") +class attrgetter: # pylint:disable=invalid-name,too-few-public-methods + """Return a callable object that fetches attr or key from its operand. + + The attribute names can also contain dots + """ + + def __init__(self, attr): + if not isinstance(attr, str): + raise TypeError("attribute name must be a string") + self._attrs = (attr,) + names = attr.split(".") + + def func(obj): + for name in names: + if hasattr(obj, name): + obj = getattr(obj, name) + elif name in obj: + obj = obj[name] + else: + raise AttributeError(f"'{type(obj).__name__}' has no attribute or item '{name}'") + return obj + + self._call = func + + def __call__(self, obj): + return self._call(obj) + + def _get_value(check_info): if "value" in check_info: value = check_info["value"] diff --git a/nautobot_design_builder/tests/testdata/custom_fields.yaml b/nautobot_design_builder/tests/testdata/custom_fields.yaml new file mode 100644 index 00000000..8382784f --- /dev/null +++ b/nautobot_design_builder/tests/testdata/custom_fields.yaml @@ -0,0 +1,54 @@ +--- +designs: + - custom_fields: + - label: "test_boolean" + name: "Test Boolean" + type: "boolean" + default: false + content_types: + - "!get:app_label": "ipam" + "!get:model": "vlan" + vlans: + - vid: 100 + name: "TEST-VLAN-100" + status__name: "Active" + custom_fields: + "Test Boolean": true + - vid: 101 + name: "TEST-VLAN-101" + status__name: "Active" + custom_fields: + "Test Boolean": false + - vid: 102 + name: "TEST-VLAN-102" + status__name: "Active" + - vid: 103 + name: "TEST-VLAN-103" + status__name: "Active" + - "!update:vid": 103 + custom_fields: + "Test Boolean": true +checks: + - equal: + - model: "nautobot.ipam.models.VLAN" + query: {vid: 100} + attribute: "cf.Test Boolean" + - value: true + + - equal: + - model: "nautobot.ipam.models.VLAN" + query: {vid: 101} + attribute: "cf.Test Boolean" + - value: false + + - equal: + - model: "nautobot.ipam.models.VLAN" + query: {vid: 102} + attribute: "cf.Test Boolean" + - value: false + + - equal: + - model: "nautobot.ipam.models.VLAN" + query: {vid: 103} + attribute: "cf.Test Boolean" + - value: true From 387f2da616e9a03becb65a792d18e437824e8273 Mon Sep 17 00:00:00 2001 From: Andrew Bates Date: Fri, 27 Sep 2024 10:11:56 -0400 Subject: [PATCH 8/9] Updated test for Nautobot 2 --- .../tests/testdata/custom_fields.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nautobot_design_builder/tests/testdata/custom_fields.yaml b/nautobot_design_builder/tests/testdata/custom_fields.yaml index 8382784f..f423b515 100644 --- a/nautobot_design_builder/tests/testdata/custom_fields.yaml +++ b/nautobot_design_builder/tests/testdata/custom_fields.yaml @@ -1,8 +1,8 @@ --- designs: - custom_fields: - - label: "test_boolean" - name: "Test Boolean" + - label: "Test Boolean" + key: "test_boolean" type: "boolean" default: false content_types: @@ -13,12 +13,12 @@ designs: name: "TEST-VLAN-100" status__name: "Active" custom_fields: - "Test Boolean": true + "test_boolean": true - vid: 101 name: "TEST-VLAN-101" status__name: "Active" custom_fields: - "Test Boolean": false + "test_boolean": false - vid: 102 name: "TEST-VLAN-102" status__name: "Active" @@ -27,28 +27,28 @@ designs: status__name: "Active" - "!update:vid": 103 custom_fields: - "Test Boolean": true + "test_boolean": true checks: - equal: - model: "nautobot.ipam.models.VLAN" query: {vid: 100} - attribute: "cf.Test Boolean" + attribute: "cf.test_boolean" - value: true - equal: - model: "nautobot.ipam.models.VLAN" query: {vid: 101} - attribute: "cf.Test Boolean" + attribute: "cf.test_boolean" - value: false - equal: - model: "nautobot.ipam.models.VLAN" query: {vid: 102} - attribute: "cf.Test Boolean" + attribute: "cf.test_boolean" - value: false - equal: - model: "nautobot.ipam.models.VLAN" query: {vid: 103} - attribute: "cf.Test Boolean" + attribute: "cf.test_boolean" - value: true From 79668f61a68b807d8957bfdccd5d17396133991b Mon Sep 17 00:00:00 2001 From: Andrew Bates Date: Tue, 1 Oct 2024 09:04:00 -0400 Subject: [PATCH 9/9] fix: Fixes most likely issues with attribute collisions This fixes an issue where some Nautobot models have an `environment` or `metadata` field name. Almost all instance attributes in `ModelInstance` have been moved into the `ModelMetadata` class, and other fields have been prefixed to reduce the likelyhood of collision. Fixes #160 --- nautobot_design_builder/contrib/ext.py | 22 +- nautobot_design_builder/debug.py | 16 +- nautobot_design_builder/design.py | 306 ++++++++++--------- nautobot_design_builder/errors.py | 10 +- nautobot_design_builder/ext.py | 7 +- nautobot_design_builder/fields.py | 56 ++-- nautobot_design_builder/tests/test_errors.py | 4 +- 7 files changed, 220 insertions(+), 201 deletions(-) diff --git a/nautobot_design_builder/contrib/ext.py b/nautobot_design_builder/contrib/ext.py index acaf30f2..8689106b 100644 --- a/nautobot_design_builder/contrib/ext.py +++ b/nautobot_design_builder/contrib/ext.py @@ -66,7 +66,7 @@ def _flatten(query: dict, prefix="") -> Iterator[Tuple[str, Any]]: yield from LookupMixin._flatten(value, f"{prefix}{key}__") else: if isinstance(value, ModelInstance): - yield (f"!get:{prefix}{key}", value.instance) + yield (f"!get:{prefix}{key}", value.design_instance) else: yield (f"!get:{prefix}{key}", value) @@ -121,7 +121,7 @@ def lookup(self, queryset, query, parent: ModelInstance = None): try: model_class = self.environment.model_class_index[queryset.model] if parent: - return parent.create_child(model_class, query, queryset) + return parent.design_metadata.create_child(model_class, query, queryset) return model_class(self.environment, query, queryset) except ObjectDoesNotExist: # pylint: disable=raise-missing-from @@ -289,9 +289,9 @@ def attribute(self, value, model_instance) -> None: { "termination_a": model_instance, "!create_or_update:termination_b_type_id": ContentType.objects.get_for_model( - remote_instance.instance + remote_instance.design_instance ).id, - "!create_or_update:termination_b_id": remote_instance.instance.id, + "!create_or_update:termination_b_id": remote_instance.design_instance.id, "deferred": True, } ) @@ -437,7 +437,7 @@ def attribute(self, value: dict, model_instance) -> None: if parent is None: raise DesignImplementationError("the child_prefix tag requires a parent") if isinstance(parent, ModelInstance): - parent = str(parent.instance.prefix) + parent = str(parent.design_instance.prefix) elif not isinstance(parent, str): raise DesignImplementationError("parent prefix must be either a previously created object or a string.") @@ -528,8 +528,8 @@ def attribute(self, value, model_instance) -> None: peering_a = None peering_z = None try: - peering_a = endpoint_a.instance.peering - peering_z = endpoint_z.instance.peering + peering_a = endpoint_a.design_instance.peering + peering_z = endpoint_z.design_instance.peering except self.Peering.model_class.DoesNotExist: pass @@ -544,13 +544,13 @@ def attribute(self, value, model_instance) -> None: peering_z.delete() retval["endpoints"] = [endpoint_a, endpoint_z] - endpoint_a.metadata.attributes["peering"] = model_instance - endpoint_z.metadata.attributes["peering"] = model_instance + endpoint_a.design_metadata.attributes["peering"] = model_instance + endpoint_z.design_metadata.attributes["peering"] = model_instance def post_save(): peering_instance: ModelInstance = model_instance - endpoint_a = peering_instance.instance.endpoint_a - endpoint_z = peering_instance.instance.endpoint_z + endpoint_a = peering_instance.design_instance.endpoint_a + endpoint_z = peering_instance.design_instance.endpoint_z endpoint_a.peer, endpoint_z.peer = endpoint_z, endpoint_a endpoint_a.save() endpoint_z.save() diff --git a/nautobot_design_builder/debug.py b/nautobot_design_builder/debug.py index b627edca..a7fd4c62 100644 --- a/nautobot_design_builder/debug.py +++ b/nautobot_design_builder/debug.py @@ -8,9 +8,9 @@ class ObjDetails: # noqa:D101 # pylint:disable=too-few-public-methods,missing-class-docstring def __init__(self, obj): # noqa:D107 # pylint:disable=missing-function-docstring - self.instance = obj - if hasattr(obj, "instance"): - self.instance = obj.instance + self.design_instance = obj + if hasattr(obj, "design_instance"): + self.design_instance = obj.design_instance try: description = str(obj) if description.startswith(" Mapping: retval = {} for key, value in query.items(): if isinstance(value, ModelInstance): - retval[key] = value.instance + retval[key] = value.design_instance elif isinstance(value, Mapping): retval[key] = _map_query_values(value) else: @@ -122,7 +122,7 @@ class ModelMetadata: # pylint: disable=too-many-instance-attributes ACTION_CHOICES = [GET, CREATE, UPDATE, CREATE_OR_UPDATE] - def __init__(self, model_instance: "ModelInstance", **kwargs): + def __init__(self, model_instance: "ModelInstance", environment: "Environment", **kwargs): """Initialize the metadata object for a given model instance. By default, the metadata object doesn't really have anything in it. In order @@ -131,9 +131,11 @@ def __init__(self, model_instance: "ModelInstance", **kwargs): Args: model_instance (ModelInstance): The model instance to which this metadata refers. + environment (Environment): The implementation environment being used for the current + design. """ self.model_instance = model_instance - self.environment = model_instance.environment + self.environment = environment self.created = False @@ -155,6 +157,11 @@ def __init__(self, model_instance: "ModelInstance", **kwargs): self._filter = {} self._kwargs = {} + @property + def import_mode(self) -> bool: + """Indicates whether the underlying environment is in import mode or not.""" + return self.environment.import_mode + @property def action(self) -> str: """Determine the action. @@ -258,7 +265,7 @@ def attributes(self, attributes: Dict[str, Any]): elif not hasattr(self.model_instance, key): value = self._attributes.pop(key) if isinstance(value, ModelInstance): - value = value.instance + value = value.design_instance self._kwargs[key] = value def connect(self, signal: str, handler: FunctionType): @@ -278,7 +285,112 @@ def send(self, signal: str): """ for handler in self._signals[signal]: handler() - self.model_instance.instance.refresh_from_db() + self.model_instance.design_instance.refresh_from_db() + + def create_child( + self, + model_class: "ModelInstance", + attributes: Dict, + relationship_manager: Manager = None, + ) -> "ModelInstance": + """Create a new ModelInstance that is linked to the current instance. + + Args: + model_class (Type[Model]): Class of the child model. + attributes (Dict): Design attributes for the child. + relationship_manager (Manager): Database relationship manager to use for the new instance. + + Returns: + ModelInstance: Model instance that has its parent correctly set. + """ + if not issubclass(model_class, ModelInstance): + model_class = self.environment.model_class_index[model_class] + try: + model_instance = model_class( + self.environment, + attributes, + relationship_manager, + parent=self, + ) + # Add the newly created instance to the log so we can keep track of + # it belonging to a design. + self.environment.journal.log(model_instance) + return model_instance + except MultipleObjectsReturned: + # pylint: disable=raise-missing-from + raise errors.DesignImplementationError( + f"Expected exactly 1 object for {model_class.__name__}({attributes}) but got more than one" + ) + except ObjectDoesNotExist: + query = ",".join([f'{k}="{v}"' for k, v in attributes.items()]) + # pylint: disable=raise-missing-from + raise errors.DesignImplementationError(f"Could not find {model_class.__name__}: {query}") + + def load_instance(self): # pylint: disable=too-many-branches + """Load the model instance's design instance from the database. + + This method will either create a new object or load an existing object + from the database, based on the action tags and query strings specified + in the design. + """ + # Short circuit if the instance was loaded earlier in + # the initialization process + if self.model_instance.design_instance is not None: + return + + query_filter = self.query_filter + field_values = self.query_filter_values + if self.action == ModelMetadata.GET: + self.model_instance.design_instance = self.model_instance.model_class.objects.get(**query_filter) + return + + if self.action in [ModelMetadata.UPDATE, ModelMetadata.CREATE_OR_UPDATE]: + # perform nested lookups. First collect all the + # query params for top-level relationships, then + # perform the actual lookup + for query_param in list(query_filter.keys()): + if "__" in query_param: + value = query_filter.pop(query_param) + attribute, filter_param = query_param.split("__", 1) + query_filter.setdefault(attribute, {}) + query_filter[attribute][f"!get:{filter_param}"] = value + + for query_param, value in query_filter.items(): + if isinstance(value, Mapping): + rel: Manager = getattr(self.model_instance.model_class, query_param) + queryset: QuerySet = rel.get_queryset() + + model = self.create_child( + self.environment.model_class_index[queryset.model], + value, + relationship_manager=queryset, + ) + if model.design_metadata.action != ModelMetadata.GET: + model.save() + query_filter[query_param] = model.design_instance + field_values[query_param] = model + try: + self.model_instance.design_instance = self.model_instance.relationship_manager.get(**query_filter) + return + except ObjectDoesNotExist: + if self.action == ModelMetadata.UPDATE: + # pylint: disable=raise-missing-from + raise errors.DesignImplementationError( + f"No match with {query_filter}", self.model_instance.model_class + ) + elif self.action != ModelMetadata.CREATE: + raise errors.DesignImplementationError( + f"Unknown database action {self.action}", self.model_instance.model_class + ) + # since the object was not found, we need to + # put the search criteria back into the attributes + # so that they will be set when the object is created + self.attributes.update(field_values) + self.created = True + try: + self.model_instance.design_instance = self.model_instance.model_class(**self.kwargs) + except TypeError as ex: + raise errors.DesignImplementationError(str(ex), self.model_instance.model_class) @property def custom_fields(self) -> Dict[str, Any]: @@ -366,6 +478,26 @@ def query_filter(self) -> Dict[str, Any]: return _map_query_values(self._filter) +def _refresh_custom_relationships(instance: "ModelInstance"): + """Look for any custom relationships for this model class and add any new fields.""" + for direction in Relationship.objects.get_for_model(instance.model_class): + for relationship in direction: + field = field_factory(instance, relationship) + + # make sure not to mask non-custom relationship fields that + # may have the same key name or field name + for attr_name in [field.key_name, field.field_name]: + if hasattr(instance.__class__, attr_name): + # if there is already an attribute with the same name, + # delete it if it is a custom relationship, that way + # we reload the config from the database. + if isinstance(getattr(instance.__class__, attr_name), CustomRelationshipField): + delattr(instance.__class__, attr_name) + + if not hasattr(instance.__class__, attr_name): + setattr(instance.__class__, attr_name, field) + + class ModelInstance: """An individual object to be created or updated as Design Builder iterates through a rendered design YAML file. @@ -425,83 +557,28 @@ def __init__( errors.MultipleObjectsReturnedError: If the object is being retrieved or updated (not created) and more than one object matches the lookup criteria. """ - self.environment = environment - self.instance: Model = None - self.metadata = ModelMetadata(self, **attributes.pop("model_metadata", {})) - - self._parent = parent - self.refresh_custom_relationships() + self.design_instance: Model = None + self.design_metadata = ModelMetadata(self, environment, **attributes.pop("model_metadata", {})) + self._design_instance_parent = parent + _refresh_custom_relationships(self) self.relationship_manager = relationship_manager if self.relationship_manager is None: self.relationship_manager = self.model_class.objects - self.metadata.attributes = attributes + self.design_metadata.attributes = attributes try: - self._load_instance() + self.design_metadata.load_instance() except ObjectDoesNotExist as ex: raise errors.DoesNotExistError(self) from ex except MultipleObjectsReturned as ex: raise errors.MultipleObjectsReturnedError(self) from ex self._update_fields() - def refresh_custom_relationships(self): - """Look for any custom relationships for this model class and add any new fields.""" - for direction in Relationship.objects.get_for_model(self.model_class): - for relationship in direction: - field = field_factory(self, relationship) - - # make sure not to mask non-custom relationship fields that - # may have the same key name or field name - for attr_name in [field.key_name, field.field_name]: - if hasattr(self.__class__, attr_name): - # if there is already an attribute with the same name, - # delete it if it is a custom relationship, that way - # we reload the config from the database. - if isinstance(getattr(self.__class__, attr_name), CustomRelationshipField): - delattr(self.__class__, attr_name) - if not hasattr(self.__class__, attr_name): - setattr(self.__class__, attr_name, field) - def __str__(self): """Get the model class name.""" return str(self.model_class) - def create_child( - self, - model_class: "ModelInstance", - attributes: Dict, - relationship_manager: Manager = None, - ) -> "ModelInstance": - """Create a new ModelInstance that is linked to the current instance. - - Args: - model_class (Type[Model]): Class of the child model. - attributes (Dict): Design attributes for the child. - relationship_manager (Manager): Database relationship manager to use for the new instance. - - Returns: - ModelInstance: Model instance that has its parent correctly set. - """ - if not issubclass(model_class, ModelInstance): - model_class = self.environment.model_class_index[model_class] - try: - return model_class( - self.environment, - attributes, - relationship_manager, - parent=self, - ) - except MultipleObjectsReturned: - # pylint: disable=raise-missing-from - raise errors.DesignImplementationError( - f"Expected exactly 1 object for {model_class.__name__}({attributes}) but got more than one" - ) - except ObjectDoesNotExist: - query = ",".join([f'{k}="{v}"' for k, v in attributes.items()]) - # pylint: disable=raise-missing-from - raise errors.DesignImplementationError(f"Could not find {model_class.__name__}: {query}") - def connect(self, signal: str, handler: FunctionType): """Connect a handler between this model instance (as sender) and signal. @@ -509,79 +586,28 @@ def connect(self, signal: str, handler: FunctionType): signal (Signal): Signal to listen for. handler (FunctionType): Callback function """ - self.metadata.connect(signal, handler) + self.design_metadata.connect(signal, handler) def _send(self, signal: str): - self.metadata.send(signal) - - def _load_instance(self): - query_filter = self.metadata.query_filter - field_values = self.metadata.query_filter_values - if self.metadata.action == ModelMetadata.GET: - self.instance = self.model_class.objects.get(**query_filter) - return - - if self.metadata.action in [ModelMetadata.UPDATE, ModelMetadata.CREATE_OR_UPDATE]: - # perform nested lookups. First collect all the - # query params for top-level relationships, then - # perform the actual lookup - for query_param in list(query_filter.keys()): - if "__" in query_param: - value = query_filter.pop(query_param) - attribute, filter_param = query_param.split("__", 1) - query_filter.setdefault(attribute, {}) - query_filter[attribute][f"!get:{filter_param}"] = value - - for query_param, value in query_filter.items(): - if isinstance(value, Mapping): - rel: Manager = getattr(self.model_class, query_param) - queryset: QuerySet = rel.get_queryset() - - model = self.create_child( - self.environment.model_class_index[queryset.model], - value, - relationship_manager=queryset, - ) - if model.metadata.action != ModelMetadata.GET: - model.save() - query_filter[query_param] = model.instance - field_values[query_param] = model - try: - self.instance = self.relationship_manager.get(**query_filter) - return - except ObjectDoesNotExist: - if self.metadata.action == ModelMetadata.UPDATE: - # pylint: disable=raise-missing-from - raise errors.DesignImplementationError(f"No match with {query_filter}", self.model_class) - elif self.metadata.action != ModelMetadata.CREATE: - raise errors.DesignImplementationError(f"Unknown database action {self.metadata.action}", self.model_class) - # since the object was not found, we need to - # put the search criteria back into the attributes - # so that they will be set when the object is created - self.metadata.attributes.update(field_values) - self.metadata.created = True - try: - self.instance = self.model_class(**self.metadata.kwargs) - except TypeError as ex: - raise errors.DesignImplementationError(str(ex), self.model_class) + self.design_metadata.send(signal) def _update_fields(self): - if self.metadata.action == ModelMetadata.GET: - if self.metadata.attributes: + if self.design_metadata.action == ModelMetadata.GET: + if self.design_metadata.attributes: # TODO: Raise a DesignModelError from here. Currently the DesignModelError doesn't # include a message. raise errors.DesignImplementationError( "Cannot update fields when using the GET action", self.model_class ) - for field_name, value in self.metadata.attributes.items(): + for field_name, value in self.design_metadata.attributes.items(): if hasattr(self.__class__, field_name): setattr(self, field_name, value) - elif hasattr(self.instance, field_name): - setattr(self.instance, field_name, value) + elif hasattr(self.design_instance, field_name): + setattr(self.design_instance, field_name, value) - for key, value in self.metadata.custom_fields.items(): - self.set_custom_field(key, value) + for key, value in self.design_metadata.custom_fields.items(): + self.design_instance.cf[key] = value def save(self): """Save the model instance to the database. @@ -590,34 +616,30 @@ def save(self): will send signals (`PRE_SAVE`, `POST_INSTANCE_SAVE` and `POST_SAVE`). The design journal is updated in this step. """ - if self.metadata.action == ModelMetadata.GET: + if self.design_metadata.action == ModelMetadata.GET: return self._send(ModelMetadata.PRE_SAVE) - msg = "Created" if self.metadata.created else "Updated" + msg = "Created" if self.design_metadata.created else "Updated" try: - self.instance.full_clean() - self.instance.save(**self.metadata.save_args) - self.environment.journal.log(self) - self.metadata.created = False - if self._parent is None: - self.environment.log_success( - message=f"{msg} {self.model_class.__name__} {self.instance}", obj=self.instance + self.design_instance.full_clean() + self.design_instance.save(**self.design_metadata.save_args) + self.design_metadata.environment.journal.log(self) + self.design_metadata.created = False + if self._design_instance_parent is None: + self.design_metadata.environment.log_success( + message=f"{msg} {self.model_class.__name__} {self.design_instance}", obj=self.design_instance ) # Refresh from DB so that we update based on any # post save signals that may have fired. - self.instance.refresh_from_db() + self.design_instance.refresh_from_db() except ValidationError as validation_error: raise errors.DesignValidationError(self) from validation_error self._send(ModelMetadata.POST_INSTANCE_SAVE) self._send(ModelMetadata.POST_SAVE) - def set_custom_field(self, field, value): - """Sets a value for a custom field.""" - self.instance.cf[field] = value - # Don't add models from these app_labels to the # object creator's list of top level models diff --git a/nautobot_design_builder/errors.py b/nautobot_design_builder/errors.py index 678324e0..150410fa 100644 --- a/nautobot_design_builder/errors.py +++ b/nautobot_design_builder/errors.py @@ -52,7 +52,7 @@ def __init__(self, model=None, parent=None) -> None: @staticmethod def _model_str(model): instance_str = None - if not isinstance(model, Model) and not hasattr(model, "instance"): + if not isinstance(model, Model) and not hasattr(model, "design_instance"): if isclass(model): return model.__name__ try: @@ -67,9 +67,9 @@ def _model_str(model): model_class = model.__class__ # if it looks like a duck... - if hasattr(model, "instance"): + if hasattr(model, "design_instance"): model_class = model.model_class - model = model.instance + model = model.design_instance if model: try: @@ -112,8 +112,8 @@ def path_str(self): model = self.model while model is not None: path_msg.insert(0, DesignModelError._model_str(model)) - if not isclass(model) and hasattr(model, "_parent"): - model = model._parent # pylint:disable=protected-access + if not isclass(model) and hasattr(model, "_design_instance_parent"): + model = model._design_instance_parent # pylint:disable=protected-access elif self.parent: model = self.parent self.parent = None diff --git a/nautobot_design_builder/ext.py b/nautobot_design_builder/ext.py index b197204e..99d0d819 100644 --- a/nautobot_design_builder/ext.py +++ b/nautobot_design_builder/ext.py @@ -195,12 +195,13 @@ def value(self, key) -> "ModelInstance": except KeyError: # pylint: disable=raise-missing-from raise DesignImplementationError(f"No ref named {key} has been saved in the design.") - if model_instance.instance and not model_instance.instance._state.adding: # pylint: disable=protected-access - model_instance.instance.refresh_from_db() + adding = model_instance.design_instance._state.adding # pylint: disable=protected-access + if model_instance.design_instance and not adding: + model_instance.design_instance.refresh_from_db() if attribute: # TODO: I think the result of the reduce operation needs to (potentially) # be wrapped up in a ModelInstance object - return reduce(getattr, [model_instance.instance, *attribute.split(".")]) + return reduce(getattr, [model_instance.design_instance, *attribute.split(".")]) return model_instance diff --git a/nautobot_design_builder/fields.py b/nautobot_design_builder/fields.py index e0bb1f10..35c27b9f 100644 --- a/nautobot_design_builder/fields.py +++ b/nautobot_design_builder/fields.py @@ -85,9 +85,9 @@ def __get__(self, obj, objtype=None) -> Any: Returns: Any: Either the descriptor instance or the field value. """ - if obj is None or obj.instance is None: + if obj is None or obj.design_instance is None: return self - return getattr(obj.instance, self.field_name) + return getattr(obj.design_instance, self.field_name) @abstractmethod def __set__(self, obj: "ModelInstance", value): @@ -134,7 +134,7 @@ class SimpleField(BaseModelField): # pylint:disable=too-few-public-methods @debug_set def __set__(self, obj: "ModelInstance", value): # noqa: D105 - setattr(obj.instance, self.field_name, value) + setattr(obj.design_instance, self.field_name, value) class RelationshipFieldMixin: # pylint:disable=too-few-public-methods @@ -170,7 +170,7 @@ def _get_instance( if related_model is None: related_model = self.related_model if isinstance(value, Mapping): - value = obj.create_child(related_model, value, relationship_manager) + value = obj.design_metadata.create_child(related_model, value, relationship_manager) return value @@ -183,11 +183,11 @@ def __set__(self, obj: "ModelInstance", value): # noqa: D105 def setter(): model_instance = self._get_instance(obj, value) - if model_instance.metadata.created: + if model_instance.design_metadata.created: model_instance.save() - setattr(obj.instance, self.field_name, model_instance.instance) + setattr(obj.design_instance, self.field_name, model_instance.design_instance) if deferred: - obj.instance.save(update_fields=[self.field_name]) + obj.design_instance.save(update_fields=[self.field_name]) if deferred: obj.connect("POST_INSTANCE_SAVE", setter) @@ -206,7 +206,7 @@ def __set__(self, obj: "ModelInstance", values): # noqa:D105 def setter(): for value in values: value = self._get_instance(obj, value, getattr(obj, self.field_name)) - setattr(value.instance, self.field.field.name, obj.instance) + setattr(value.design_instance, self.field.field.name, obj.design_instance) value.save() obj.connect("POST_INSTANCE_SAVE", setter) @@ -270,17 +270,15 @@ def setter(): items = [] for value in values: related_model = self._get_related_model(value) - value = self._get_instance(obj, value, getattr(obj.instance, self.field_name), related_model) + value = self._get_instance(obj, value, getattr(obj.design_instance, self.field_name), related_model) if related_model is not self.through: - items.append(value.instance) + items.append(value.design_instance) else: - setattr(value.instance, self.link_field, obj.instance) - if value.metadata.created: + setattr(value.design_instance, self.link_field, obj.design_instance) + if value.design_metadata.created: value.save() - else: - value.environment.journal.log(value) if items: - getattr(obj.instance, self.field_name).add(*items) + getattr(obj.design_instance, self.field_name).add(*items) obj.connect("POST_INSTANCE_SAVE", setter) @@ -310,10 +308,10 @@ def __set__(self, obj: "ModelInstance", values): # noqa:D105 items = [] for value in values: value = self._get_instance(obj, value) - if value.metadata.created: + if value.design_metadata.created: value.save() - items.append(value.instance) - getattr(obj.instance, self.field_name).add(*items) + items.append(value.desig_instance) + getattr(obj.design_instance, self.field_name).add(*items) class GenericForeignKeyField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods @@ -323,8 +321,8 @@ class GenericForeignKeyField(BaseModelField, RelationshipFieldMixin): # pylint: def __set__(self, obj: "ModelInstance", value): # noqa:D105 fk_field = self.field.fk_field ct_field = self.field.ct_field - setattr(obj.instance, fk_field, value.instance.pk) - setattr(obj.instance, ct_field, ContentType.objects.get_for_model(value.instance)) + setattr(obj.design_instance, fk_field, value.design_instance.pk) + setattr(obj.design_instance, ct_field, ContentType.objects.get_for_model(value.design_instance)) class TagField(BaseModelField, RelationshipFieldMixin): # pylint:disable=too-few-public-methods @@ -341,13 +339,11 @@ def __set__(self, obj: "ModelInstance", values): # noqa:D105 def setter(): items = [] for value in values: - value = self._get_instance(obj, value, getattr(obj.instance, self.field_name)) - if value.metadata.created: + value = self._get_instance(obj, value, getattr(obj.design_instance, self.field_name)) + if value.design_metadata.created: value.save() - else: - value.environment.journal.log(value) - items.append(value.instance) - getattr(obj.instance, self.field_name).add(*items) + items.append(value.design_instance) + getattr(obj.design_instance, self.field_name).add(*items) obj.connect("POST_INSTANCE_SAVE", setter) @@ -357,7 +353,7 @@ class GenericRelField(BaseModelField, RelationshipFieldMixin): # pylint:disable @debug_set def __set__(self, obj: "ModelInstance", value): # noqa:D105 - setattr(obj.instance, self.field.attname, self._get_instance(obj, value)) + setattr(obj.design_instance, self.field.attname, self._get_instance(obj, value)) class CustomRelationshipField(ModelField, RelationshipFieldMixin): # pylint: disable=too-few-public-methods @@ -392,11 +388,11 @@ def __set__(self, obj: "ModelInstance", values): # noqa:D105 def setter(): for value in values: value = self._get_instance(obj, value) - if value.metadata.created: + if value.design_metadata.created: value.save() - source = obj.instance - destination = value.instance + source = obj.design_instance + destination = value.design_instance if self.relationship.source_type == ContentType.objects.get_for_model(destination): source, destination = destination, source diff --git a/nautobot_design_builder/tests/test_errors.py b/nautobot_design_builder/tests/test_errors.py index 9df582e5..17e7e328 100644 --- a/nautobot_design_builder/tests/test_errors.py +++ b/nautobot_design_builder/tests/test_errors.py @@ -15,11 +15,11 @@ class TestModel: # pylint:disable=too-few-public-methods def __init__(self, title="", parent=None): self.title = title - self.instance = self + self.design_instance = self self.model_class = self self._meta = self self.verbose_name = "verbose name" - self._parent = parent + self._design_instance_parent = parent def __str__(self): return self.title