From 1eaeb7dff33f046717028f1d3409228acd2f1dcb Mon Sep 17 00:00:00 2001 From: Frederico Sabino <3332770+fmrsabino@users.noreply.github.com> Date: Mon, 3 May 2021 13:52:57 +0200 Subject: [PATCH] Add Black code style formatter (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds Black code formatter – https://black.readthedocs.io/en/stable/ - Applied the formatting to the project - Updated flake8 config to be compatible with Black – https://black.readthedocs.io/en/stable/compatible_configs.html#flake8 - Adds Black check to the CI pipeline – Note that this does not automatically format any code. Code should be formatted on the user side either manually or by running black src/ to apply the suggestions automatically --- .flake8 | 2 +- requirements-dev.txt | 1 + run | 8 +- src/config/asgi.py | 2 +- src/config/settings.py | 82 +++++------ src/config/urls.py | 4 +- src/config/wsgi.py | 2 +- src/manage.py | 4 +- src/safe_apps/admin.py | 18 +-- src/safe_apps/apps.py | 4 +- src/safe_apps/migrations/0001_initial.py | 35 +++-- src/safe_apps/models.py | 4 +- src/safe_apps/serializers.py | 4 +- src/safe_apps/tests/factories.py | 14 +- src/safe_apps/tests/test_views.py | 171 +++++++++++------------ src/safe_apps/urls.py | 4 +- src/safe_apps/views.py | 2 +- 17 files changed, 187 insertions(+), 174 deletions(-) diff --git a/.flake8 b/.flake8 index ce36a22e..2ef8baf6 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length = 119 - +extend-ignore = E203, W503 exclude = src/safe_apps/migrations diff --git a/requirements-dev.txt b/requirements-dev.txt index b16e94e7..f14b648b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -r requirements.txt +black==21.4b2 factory-boy==3.2.0 flake8==3.9.1 pytest-django==4.2.0 diff --git a/run b/run index d826a5f7..46953599 100755 --- a/run +++ b/run @@ -41,6 +41,11 @@ function test { cmd pytest "${@}" } +function black { + # Run Black code formatter check + cmd black "${@}" +} + function flake8 { # Lint Python code with flake8 cmd flake8 "${@}" @@ -56,7 +61,8 @@ function ci:test { . .env manage check - flake8 "${@}" + black --check . + flake8 . manage migrate test } diff --git a/src/config/asgi.py b/src/config/asgi.py index 0084eb45..e6262aba 100644 --- a/src/config/asgi.py +++ b/src/config/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_asgi_application() diff --git a/src/config/settings.py b/src/config/settings.py index 885a3eb7..d071cd8e 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -32,57 +32,57 @@ # Application definition INSTALLED_APPS = [ - 'safe_apps.apps.AppsConfig', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', + "safe_apps.apps.AppsConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'config.urls' +ROOT_URLCONF = "config.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'config.wsgi.application' +WSGI_APPLICATION = "config.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv("POSTGRES_NAME", "postgres"), - 'USER': os.getenv("POSTGRES_USER", "postgres"), - 'PASSWORD': os.getenv("POSTGRES_PASSWORD", "postgres"), - 'HOST': os.getenv("POSTGRES_HOST", "db"), - 'PORT': os.getenv("POSTGRES_PORT", "5432"), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("POSTGRES_NAME", "postgres"), + "USER": os.getenv("POSTGRES_USER", "postgres"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD", "postgres"), + "HOST": os.getenv("POSTGRES_HOST", "db"), + "PORT": os.getenv("POSTGRES_PORT", "5432"), } } @@ -91,25 +91,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -120,9 +120,9 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/src/config/urls.py b/src/config/urls.py index 6b41be01..d4666d82 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -2,6 +2,6 @@ from django.urls import path, include urlpatterns = [ - path('api/v1/', include('safe_apps.urls', namespace='v1')), - path('admin/', admin.site.urls), + path("api/v1/", include("safe_apps.urls", namespace="v1")), + path("admin/", admin.site.urls), ] diff --git a/src/config/wsgi.py b/src/config/wsgi.py index fcfb0f24..9c2606d5 100644 --- a/src/config/wsgi.py +++ b/src/config/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_wsgi_application() diff --git a/src/manage.py b/src/manage.py index 8e7ac79b..d28672ea 100755 --- a/src/manage.py +++ b/src/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/safe_apps/admin.py b/src/safe_apps/admin.py index d6b6b58f..11f1dbae 100644 --- a/src/safe_apps/admin.py +++ b/src/safe_apps/admin.py @@ -4,11 +4,11 @@ class NetworksFilter(admin.SimpleListFilter): - title = 'Networks' - parameter_name = 'networks' + title = "Networks" + parameter_name = "networks" def lookups(self, request, model_admin): - values = SafeApp.objects.values_list('networks', flat=True) + values = SafeApp.objects.values_list("networks", flat=True) # lookups requires a tuple to be returned – (value, verbose value) networks = [(network, network) for networks in values for network in networks] networks = sorted(set(networks)) @@ -22,14 +22,14 @@ def queryset(self, request, queryset): @admin.register(SafeApp) class SafeAppAdmin(admin.ModelAdmin): - list_display = ('name', 'url', 'networks') + list_display = ("name", "url", "networks") list_filter = (NetworksFilter,) - search_fields = ('name', 'url') - ordering = ('name',) + search_fields = ("name", "url") + ordering = ("name",) @admin.register(Provider) class ProviderAdmin(admin.ModelAdmin): - list_display = ('name', 'url') - search_fields = ('name',) - ordering = ('name',) + list_display = ("name", "url") + search_fields = ("name",) + ordering = ("name",) diff --git a/src/safe_apps/apps.py b/src/safe_apps/apps.py index f5816c85..1d8b5355 100644 --- a/src/safe_apps/apps.py +++ b/src/safe_apps/apps.py @@ -2,5 +2,5 @@ class AppsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'safe_apps' + default_auto_field = "django.db.models.BigAutoField" + name = "safe_apps" diff --git a/src/safe_apps/migrations/0001_initial.py b/src/safe_apps/migrations/0001_initial.py index fbaeb356..bf724df8 100644 --- a/src/safe_apps/migrations/0001_initial.py +++ b/src/safe_apps/migrations/0001_initial.py @@ -9,26 +9,37 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Provider', + name="Provider", fields=[ - ('url', models.URLField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200)), + ("url", models.URLField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=200)), ], ), migrations.CreateModel( - name='SafeApp', + name="SafeApp", fields=[ - ('url', models.URLField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200)), - ('icon_url', models.URLField()), - ('description', models.CharField(max_length=200)), - ('networks', django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None)), - ('provider', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='safe_apps.provider')), + ("url", models.URLField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=200)), + ("icon_url", models.URLField()), + ("description", models.CharField(max_length=200)), + ( + "networks", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(), size=None + ), + ), + ( + "provider", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="safe_apps.provider", + ), + ), ], ), ] diff --git a/src/safe_apps/models.py b/src/safe_apps/models.py index dd5d2ca3..593ccd8d 100644 --- a/src/safe_apps/models.py +++ b/src/safe_apps/models.py @@ -7,7 +7,7 @@ class Provider(models.Model): name = models.CharField(max_length=200) def __str__(self): - return f'{self.name} | {self.url}' + return f"{self.name} | {self.url}" class SafeApp(models.Model): @@ -19,4 +19,4 @@ class SafeApp(models.Model): provider = models.ForeignKey(Provider, null=True, on_delete=models.SET_NULL) def __str__(self): - return f'{self.name} | {self.url} | networks={self.networks}' + return f"{self.name} | {self.url} | networks={self.networks}" diff --git a/src/safe_apps/serializers.py b/src/safe_apps/serializers.py index 536254b0..e398f988 100644 --- a/src/safe_apps/serializers.py +++ b/src/safe_apps/serializers.py @@ -6,7 +6,7 @@ class ProviderSerializer(serializers.ModelSerializer): class Meta: model = Provider - fields = ['url', 'name'] + fields = ["url", "name"] class SafeAppsResponseSerializer(serializers.ModelSerializer): @@ -14,4 +14,4 @@ class SafeAppsResponseSerializer(serializers.ModelSerializer): class Meta: model = SafeApp - fields = ['url', 'name', 'icon_url', 'description', 'networks', 'provider'] + fields = ["url", "name", "icon_url", "description", "networks", "provider"] diff --git a/src/safe_apps/tests/factories.py b/src/safe_apps/tests/factories.py index 95528187..af800de1 100644 --- a/src/safe_apps/tests/factories.py +++ b/src/safe_apps/tests/factories.py @@ -8,17 +8,17 @@ class ProviderFactory(DjangoModelFactory): class Meta: model = Provider - name = factory.Faker('company') - url = factory.Faker('url') + name = factory.Faker("company") + url = factory.Faker("url") class SafeAppFactory(DjangoModelFactory): class Meta: model = SafeApp - url = factory.Faker('url') - name = factory.Faker('company') - icon_url = factory.Faker('image_url') - description = factory.Faker('catch_phrase') - networks = factory.Faker('pylist', nb_elements=2, value_types=(int,)) + url = factory.Faker("url") + name = factory.Faker("company") + icon_url = factory.Faker("image_url") + description = factory.Faker("catch_phrase") + networks = factory.Faker("pylist", nb_elements=2, value_types=(int,)) provider = None diff --git a/src/safe_apps/tests/test_views.py b/src/safe_apps/tests/test_views.py index fe8c07b1..bea77f7b 100644 --- a/src/safe_apps/tests/test_views.py +++ b/src/safe_apps/tests/test_views.py @@ -8,47 +8,46 @@ class EmptySafeAppsListViewTests(APITestCase): def test_empty_set(self): - url = reverse('v1:safe-apps') + url = reverse("v1:safe-apps") - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), []) class FilterSafeAppListViewTests(APITestCase): - def test_all_safes_returned(self): (safe_app_1, safe_app_2, safe_app_3) = SafeAppFactory.create_batch(3) json_response = [ { - 'url': safe_app_1.url, - 'name': safe_app_1.name, - 'icon_url': safe_app_1.icon_url, - 'description': safe_app_1.description, - 'networks': safe_app_1.networks, - 'provider': None + "url": safe_app_1.url, + "name": safe_app_1.name, + "icon_url": safe_app_1.icon_url, + "description": safe_app_1.description, + "networks": safe_app_1.networks, + "provider": None, }, { - 'url': safe_app_2.url, - 'name': safe_app_2.name, - 'icon_url': safe_app_2.icon_url, - 'description': safe_app_2.description, - 'networks': safe_app_2.networks, - 'provider': None + "url": safe_app_2.url, + "name": safe_app_2.name, + "icon_url": safe_app_2.icon_url, + "description": safe_app_2.description, + "networks": safe_app_2.networks, + "provider": None, }, { - 'url': safe_app_3.url, - 'name': safe_app_3.name, - 'icon_url': safe_app_3.icon_url, - 'description': safe_app_3.description, - 'networks': safe_app_3.networks, - 'provider': None + "url": safe_app_3.url, + "name": safe_app_3.name, + "icon_url": safe_app_3.icon_url, + "description": safe_app_3.description, + "networks": safe_app_3.networks, + "provider": None, }, ] - url = reverse('v1:safe-apps') + url = reverse("v1:safe-apps") - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) @@ -57,33 +56,33 @@ def test_all_apps_returned_on_empty_network_value(self): (safe_app_1, safe_app_2, safe_app_3) = SafeAppFactory.create_batch(3) json_response = [ { - 'url': safe_app_1.url, - 'name': safe_app_1.name, - 'icon_url': safe_app_1.icon_url, - 'description': safe_app_1.description, - 'networks': safe_app_1.networks, - 'provider': None + "url": safe_app_1.url, + "name": safe_app_1.name, + "icon_url": safe_app_1.icon_url, + "description": safe_app_1.description, + "networks": safe_app_1.networks, + "provider": None, }, { - 'url': safe_app_2.url, - 'name': safe_app_2.name, - 'icon_url': safe_app_2.icon_url, - 'description': safe_app_2.description, - 'networks': safe_app_2.networks, - 'provider': None + "url": safe_app_2.url, + "name": safe_app_2.name, + "icon_url": safe_app_2.icon_url, + "description": safe_app_2.description, + "networks": safe_app_2.networks, + "provider": None, }, { - 'url': safe_app_3.url, - 'name': safe_app_3.name, - 'icon_url': safe_app_3.icon_url, - 'description': safe_app_3.description, - 'networks': safe_app_3.networks, - 'provider': None + "url": safe_app_3.url, + "name": safe_app_3.name, + "icon_url": safe_app_3.icon_url, + "description": safe_app_3.description, + "networks": safe_app_3.networks, + "provider": None, }, ] - url = reverse('v1:safe-apps') + f'{"?network_id="}' + url = reverse("v1:safe-apps") + f'{"?network_id="}' - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) @@ -94,25 +93,25 @@ def test_apps_returned_on_filtered_network(self): json_response = [ { - 'url': safe_app_4.url, - 'name': safe_app_4.name, - 'icon_url': safe_app_4.icon_url, - 'description': safe_app_4.description, - 'networks': safe_app_4.networks, - 'provider': None + "url": safe_app_4.url, + "name": safe_app_4.name, + "icon_url": safe_app_4.icon_url, + "description": safe_app_4.description, + "networks": safe_app_4.networks, + "provider": None, }, { - 'url': safe_app_5.url, - 'name': safe_app_5.name, - 'icon_url': safe_app_5.icon_url, - 'description': safe_app_5.description, - 'networks': safe_app_5.networks, - 'provider': None - } + "url": safe_app_5.url, + "name": safe_app_5.name, + "icon_url": safe_app_5.icon_url, + "description": safe_app_5.description, + "networks": safe_app_5.networks, + "provider": None, + }, ] - url = reverse('v1:safe-apps') + f'{"?network_id=1"}' + url = reverse("v1:safe-apps") + f'{"?network_id=1"}' - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) @@ -120,9 +119,9 @@ def test_apps_returned_on_filtered_network(self): def test_apps_returned_on_unexisting_network(self): SafeAppFactory.create_batch(3, networks=[12]) json_response = [] - url = reverse('v1:safe-apps') + f'{"?network_id=10"}' + url = reverse("v1:safe-apps") + f'{"?network_id=10"}' - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) @@ -132,44 +131,40 @@ def test_apps_returned_on_same_key_pair(self): SafeAppFactory.create(networks=[2]) json_response = [ { - 'url': safe_app_1.url, - 'name': safe_app_1.name, - 'icon_url': safe_app_1.icon_url, - 'description': safe_app_1.description, - 'networks': safe_app_1.networks, - 'provider': None + "url": safe_app_1.url, + "name": safe_app_1.name, + "icon_url": safe_app_1.icon_url, + "description": safe_app_1.description, + "networks": safe_app_1.networks, + "provider": None, } ] - url = reverse('v1:safe-apps') + f'{"?network_id=2&network_id=1"}' + url = reverse("v1:safe-apps") + f'{"?network_id=2&network_id=1"}' - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) class ProviderInfoTests(APITestCase): - def test_provider_returned_in_response(self): provider = ProviderFactory.create() safe_app = SafeAppFactory.create(provider=provider) json_response = [ { - 'url': safe_app.url, - 'name': safe_app.name, - 'icon_url': safe_app.icon_url, - 'description': safe_app.description, - 'networks': safe_app.networks, - 'provider': { - 'name': provider.name, - 'url': provider.url - } + "url": safe_app.url, + "name": safe_app.name, + "icon_url": safe_app.icon_url, + "description": safe_app.description, + "networks": safe_app.networks, + "provider": {"name": provider.name, "url": provider.url}, } ] - url = reverse('v1:safe-apps') + url = reverse("v1:safe-apps") - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) @@ -179,17 +174,17 @@ def test_provider_not_returned_in_response(self): json_response = [ { - 'url': safe_app.url, - 'name': safe_app.name, - 'icon_url': safe_app.icon_url, - 'description': safe_app.description, - 'networks': safe_app.networks, - 'provider': None + "url": safe_app.url, + "name": safe_app.name, + "icon_url": safe_app.icon_url, + "description": safe_app.description, + "networks": safe_app.networks, + "provider": None, } ] - url = reverse('v1:safe-apps') + url = reverse("v1:safe-apps") - response = self.client.get(path=url, data=None, format='json') + response = self.client.get(path=url, data=None, format="json") self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), json_response) diff --git a/src/safe_apps/urls.py b/src/safe_apps/urls.py index ff120a61..5f0e516e 100644 --- a/src/safe_apps/urls.py +++ b/src/safe_apps/urls.py @@ -2,8 +2,8 @@ from .views import SafeAppsListView -app_name = 'apps' +app_name = "apps" urlpatterns = [ - path('safe-apps/', SafeAppsListView.as_view(), name='safe-apps'), + path("safe-apps/", SafeAppsListView.as_view(), name="safe-apps"), ] diff --git a/src/safe_apps/views.py b/src/safe_apps/views.py index 6abb0015..96108a63 100644 --- a/src/safe_apps/views.py +++ b/src/safe_apps/views.py @@ -10,7 +10,7 @@ class SafeAppsListView(ListAPIView): def get_queryset(self): queryset = SafeApp.objects.all() - network_id = self.request.query_params.get('network_id') + network_id = self.request.query_params.get("network_id") if network_id is not None and network_id.isdigit(): queryset = queryset.filter(networks__contains=[network_id])