From 155a39155859d4400851fa45754747bb356d2ef8 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 30 Sep 2025 09:17:46 +0000 Subject: [PATCH 01/57] Updating getVersions Modules API to use admin API POC --- src/google/appengine/api/modules/modules.py | 29 +++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index a96e1fb..660fccf 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -54,6 +54,7 @@ from google.appengine.api import apiproxy_stub_map from google.appengine.api.modules import modules_service_pb2 from google.appengine.runtime import apiproxy_errors +from googleapiclient import discovery class Error(Exception): @@ -196,23 +197,17 @@ def get_versions(module=None): `InvalidModuleError` if the given module isn't valid, `TransientError` if there is an issue fetching the information. """ - - def _ResultHook(rpc): - mapped_errors = [ - modules_service_pb2.ModulesServiceError.INVALID_MODULE, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR - ] - _CheckAsyncResult(rpc, mapped_errors, {}) - - - return rpc.response.version - - request = modules_service_pb2.GetVersionsRequest() - if module: - request.module = module - response = modules_service_pb2.GetVersionsResponse() - return _MakeAsyncCall('GetVersions', request, response, - _ResultHook).get_result() + + project = os.environ.get('GCP_PROJECT') + if not module: + module = os.environ.get('GAE_SERVICE', 'default') + + client = discovery.build('appengine', 'v1') + request = client.apps().services().versions().list( + appsId=project_id, servicesId=service_id, view='FULL') + response = request.execute() + + return response.get('versions', []) def get_default_version(module=None): From 5c68f1e152f1d4981d17f5cd37869141e2759d8c Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 30 Sep 2025 10:42:05 +0000 Subject: [PATCH 02/57] Fixing bug --- src/google/appengine/api/modules/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 660fccf..3c900eb 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -198,13 +198,13 @@ def get_versions(module=None): there is an issue fetching the information. """ - project = os.environ.get('GCP_PROJECT') + project_id = os.environ.get('GCP_PROJECT') if not module: module = os.environ.get('GAE_SERVICE', 'default') client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( - appsId=project_id, servicesId=service_id, view='FULL') + appsId=project_id, servicesId=module, view='FULL') response = request.execute() return response.get('versions', []) From 7555a40b877b0115cfe46d696f34175455c49d4f Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 1 Oct 2025 04:46:15 +0000 Subject: [PATCH 03/57] Fixing bug again --- src/google/appengine/api/modules/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 3c900eb..007db64 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -197,11 +197,11 @@ def get_versions(module=None): `InvalidModuleError` if the given module isn't valid, `TransientError` if there is an issue fetching the information. """ - - project_id = os.environ.get('GCP_PROJECT') if not module: module = os.environ.get('GAE_SERVICE', 'default') + project_id = os.environ.get('GCP_PROJECT') + print("#################" + project_id) client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( appsId=project_id, servicesId=module, view='FULL') From cad178e2412bf166236acacd63f1a3afbe9b6ccc Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 1 Oct 2025 05:05:48 +0000 Subject: [PATCH 04/57] Fixing bug again --- src/google/appengine/api/modules/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 007db64..55ee2c3 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -200,11 +200,11 @@ def get_versions(module=None): if not module: module = os.environ.get('GAE_SERVICE', 'default') - project_id = os.environ.get('GCP_PROJECT') + project = os.environ.get('GCP_PROJECT') print("#################" + project_id) client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( - appsId=project_id, servicesId=module, view='FULL') + appsId=project, servicesId=module, view='FULL') response = request.execute() return response.get('versions', []) From ddbcb292378f3c3f809d10f43aaeb8fe69658d6d Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 1 Oct 2025 05:16:31 +0000 Subject: [PATCH 05/57] Fixing bug again --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 55ee2c3..2f684ac 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -201,7 +201,7 @@ def get_versions(module=None): module = os.environ.get('GAE_SERVICE', 'default') project = os.environ.get('GCP_PROJECT') - print("#################" + project_id) + logging.info("#################" + project_id) client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( appsId=project, servicesId=module, view='FULL') From e8a9655455865d148be059f0248565a983357c35 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 1 Oct 2025 06:20:23 +0000 Subject: [PATCH 06/57] Fixing log statement for getVersions --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 2f684ac..854bdfb 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -201,7 +201,7 @@ def get_versions(module=None): module = os.environ.get('GAE_SERVICE', 'default') project = os.environ.get('GCP_PROJECT') - logging.info("#################" + project_id) + logging.info("#################" + project) client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( appsId=project, servicesId=module, view='FULL') From 36c461ccce4f93eb679f6e1de5733987e52ed6f2 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 1 Oct 2025 06:31:35 +0000 Subject: [PATCH 07/57] trying to get project_Id from env vars --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 854bdfb..34e261c 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -200,7 +200,7 @@ def get_versions(module=None): if not module: module = os.environ.get('GAE_SERVICE', 'default') - project = os.environ.get('GCP_PROJECT') + project = os.environ.get('GAE_APPLICATION') or os.environ.get('GOOGLE_CLOUD_PROJECT') logging.info("#################" + project) client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( From 570ca5ad7920f475d7c1abdf04607c824f1ceea1 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 1 Oct 2025 07:35:26 +0000 Subject: [PATCH 08/57] tne --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 34e261c..0318a36 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -204,7 +204,7 @@ def get_versions(module=None): logging.info("#################" + project) client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( - appsId=project, servicesId=module, view='FULL') + appsId='hrithikgajera-test', servicesId='default', view='FULL') response = request.execute() return response.get('versions', []) From 03487761d174c65cda02621474927594bf7cd238 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 3 Oct 2025 06:58:06 +0000 Subject: [PATCH 09/57] refactored getVersions, getModules and getDefaultVersions --- src/google/appengine/api/modules/modules.py | 78 +++++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 0318a36..01bef36 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -169,17 +169,17 @@ def get_modules(): the name of the module that is associated with the instance that calls this function. """ - - def _ResultHook(rpc): - _CheckAsyncResult(rpc, [], {}) - - - return rpc.response.module - - request = modules_service_pb2.GetModulesRequest() - response = modules_service_pb2.GetModulesResponse() - return _MakeAsyncCall('GetModules', request, response, - _ResultHook).get_result() + project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if project is None: + appId = os.environ.get('GAE_APPLICATION') + project = appId.split('~', 1)[1] + parent = f'apps/{project}' + service = discovery.build('appengine', 'v1') + request = client.apps().services().list(parent=parent) + response = request.execute() + + return [service['id'] for service in response.get('services', [])] + def get_versions(module=None): @@ -199,15 +199,16 @@ def get_versions(module=None): """ if not module: module = os.environ.get('GAE_SERVICE', 'default') - - project = os.environ.get('GAE_APPLICATION') or os.environ.get('GOOGLE_CLOUD_PROJECT') - logging.info("#################" + project) + project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if project is None: + appId = os.environ.get('GAE_APPLICATION') + project = appId.split('~', 1)[1] client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( - appsId='hrithikgajera-test', servicesId='default', view='FULL') + appsId=project, servicesId=module, view='FULL') response = request.execute() - return response.get('versions', []) + return [version['id'] for version in response.get('versions', [])] def get_default_version(module=None): @@ -224,21 +225,36 @@ def get_default_version(module=None): `InvalidModuleError` if the given module is not valid, `InvalidVersionError` if no default version could be found. """ - - def _ResultHook(rpc): - mapped_errors = [ - modules_service_pb2.ModulesServiceError.INVALID_MODULE, - modules_service_pb2.ModulesServiceError.INVALID_VERSION - ] - _CheckAsyncResult(rpc, mapped_errors, {}) - return rpc.response.version - - request = modules_service_pb2.GetDefaultVersionRequest() - if module: - request.module = module - response = modules_service_pb2.GetDefaultVersionResponse() - return _MakeAsyncCall('GetDefaultVersion', request, response, - _ResultHook).get_result() + if not module: + module = os.environ.get('GAE_SERVICE', 'default') + project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if project is None: + appId = os.environ.get('GAE_APPLICATION') + project = appId.split('~', 1)[1] + client = discovery.build('appengine', 'v1') + request = client.apps().services().get( + appsId=project, services_id=module) + + response = request.execute() + + allocations = response.get('split', {}).get('allocations') + maxAlloc = -1 + retVersion = None + for version, allocation in allocations : + if allocation == 1.0: + retVersion = version + break + + if allocation > maxAlloc : + retVersion = version + maxAlloc = allocation + else if allocation == maxAlloc: + if version < retVersion: + retVersion = version + + return retVersion + + def get_num_instances( From 253b3ed7b4f05759cd04adb1702c0f1e285a8931 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 3 Oct 2025 07:03:51 +0000 Subject: [PATCH 10/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 01bef36..4226c9f 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -248,7 +248,7 @@ def get_default_version(module=None): if allocation > maxAlloc : retVersion = version maxAlloc = allocation - else if allocation == maxAlloc: + elif allocation == maxAlloc: if version < retVersion: retVersion = version From f1c7beb7bac4f15e6197d9b8ef151e96a4c52b78 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 3 Oct 2025 07:09:28 +0000 Subject: [PATCH 11/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 4226c9f..5c24d08 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -174,7 +174,7 @@ def get_modules(): appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] parent = f'apps/{project}' - service = discovery.build('appengine', 'v1') + client = discovery.build('appengine', 'v1') request = client.apps().services().list(parent=parent) response = request.execute() From 441d12a32205feaf3bd69d32c4be2ab6e9c48ab1 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 3 Oct 2025 10:14:42 +0000 Subject: [PATCH 12/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 5c24d08..4b28728 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -173,9 +173,9 @@ def get_modules(): if project is None: appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] - parent = f'apps/{project}' + parent = 'apps/' + project client = discovery.build('appengine', 'v1') - request = client.apps().services().list(parent=parent) + request = client.apps().services().list(parent) response = request.execute() return [service['id'] for service in response.get('services', [])] From b807cdc1b0315b858c629d1dde7fdc96653c510a Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 3 Oct 2025 10:21:52 +0000 Subject: [PATCH 13/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 4b28728..d813d09 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -175,7 +175,7 @@ def get_modules(): project = appId.split('~', 1)[1] parent = 'apps/' + project client = discovery.build('appengine', 'v1') - request = client.apps().services().list(parent) + request = client.apps().services().list(appsId=project) response = request.execute() return [service['id'] for service in response.get('services', [])] From 40ad9d21ccade5f57456687042bb4d46f613d6e1 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 3 Oct 2025 10:26:27 +0000 Subject: [PATCH 14/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index d813d09..1ca614e 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -233,7 +233,7 @@ def get_default_version(module=None): project = appId.split('~', 1)[1] client = discovery.build('appengine', 'v1') request = client.apps().services().get( - appsId=project, services_id=module) + appsId=project, servicesId=module) response = request.execute() From 4574313185ee794fa5338ff8aa79ec9118d0efcf Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 6 Oct 2025 04:51:29 +0000 Subject: [PATCH 15/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 1ca614e..a50a03f 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -240,7 +240,7 @@ def get_default_version(module=None): allocations = response.get('split', {}).get('allocations') maxAlloc = -1 retVersion = None - for version, allocation in allocations : + for version, allocation in allocations.items() : if allocation == 1.0: retVersion = version break From bad053b2fcd2f1d77f4409e448f9347038475ac6 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 7 Oct 2025 06:40:44 +0000 Subject: [PATCH 16/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 33 +++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index a50a03f..d108089 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -281,19 +281,26 @@ def get_num_instances( `InvalidVersionError` on invalid input. """ - def _ResultHook(rpc): - mapped_errors = [modules_service_pb2.ModulesServiceError.INVALID_VERSION] - _CheckAsyncResult(rpc, mapped_errors, {}) - return rpc.response.instances - - request = modules_service_pb2.GetNumInstancesRequest() - if module: - request.module = module - if version: - request.version = version - response = modules_service_pb2.GetNumInstancesResponse() - return _MakeAsyncCall('GetNumInstances', request, response, - _ResultHook).get_result() + if module is None: + module = get_current_module_name() + + if version is None: + version = get_current_version_name() + + project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if project is None: + appId = os.environ.get('GAE_APPLICATION') + project = appId.split('~', 1)[1] + + client = discover.build('appengine', 'v1') + request = service.apps().services().versions().get( + appsId=project, servicesId=module, versionsId=version) + + response = request.execute() + if 'manualScaling' in response: + return response['manualScaling'].get('instances') + + return 0 def set_num_instances( From 2254525b31336ef27004be74ed54692dd3e81129 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 7 Oct 2025 06:48:27 +0000 Subject: [PATCH 17/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index d108089..dd66bb7 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -292,7 +292,7 @@ def get_num_instances( appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] - client = discover.build('appengine', 'v1') + client = discovery.build('appengine', 'v1') request = service.apps().services().versions().get( appsId=project, servicesId=module, versionsId=version) From e05f0f7a9fcdafb404d74437779e560261004913 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 7 Oct 2025 06:55:57 +0000 Subject: [PATCH 18/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index dd66bb7..e047704 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -293,7 +293,7 @@ def get_num_instances( project = appId.split('~', 1)[1] client = discovery.build('appengine', 'v1') - request = service.apps().services().versions().get( + request = client.apps().services().versions().get( appsId=project, servicesId=module, versionsId=version) response = request.execute() From c9141faa3a8e58ab33c88f444e53cf57bd6097ed Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 7 Oct 2025 09:38:01 +0000 Subject: [PATCH 19/57] fixing bugs --- src/google/appengine/api/modules/modules.py | 44 +++++++++++---------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index e047704..b46f334 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -486,23 +486,27 @@ def get_hostname( TypeError: if the given instance type is invalid. """ - def _ResultHook(rpc): - mapped_errors = [ - modules_service_pb2.ModulesServiceError.INVALID_MODULE, - modules_service_pb2.ModulesServiceError.INVALID_INSTANCES - ] - _CheckAsyncResult(rpc, mapped_errors, []) - return rpc.response.hostname - - request = modules_service_pb2.GetHostnameRequest() - if module: - request.module = module - if version: - request.version = version - if instance or instance == 0: - if not isinstance(instance, (six.string_types, six.integer_types)): - raise TypeError("'instance' arg must be of type basestring, long or int.") - request.instance = str(instance) - response = modules_service_pb2.GetHostnameResponse() - return _MakeAsyncCall('GetHostname', request, response, - _ResultHook).get_result() + if module is None: + module = get_current_module_name() + + if version is None: + version = get_current_version_name() + + project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if project is None: + appId = os.environ.get('GAE_APPLICATION') + project = appId.split('~', 1)[1] + + client = discovery.build('appengine', 'v1') + request = client.apps().get(appsId=project) + response = request.execute() + default_hostname = response.get('defaultHostname') + + hostname_parts = [] + if instance: + hostname_parts.append(instance) + hostname_parts.append(version) + hostname_parts.append(module) + hostname_parts.append(default_hostname) + + return ".".join(hostname_parts) From 4e02dfe481bd41736b0327b5577546365eb32195 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 16 Oct 2025 11:19:55 +0000 Subject: [PATCH 20/57] Using region specific client for admin api --- src/google/appengine/api/modules/modules.py | 69 ++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index b46f334..f8fc53e 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -55,6 +55,8 @@ from google.appengine.api.modules import modules_service_pb2 from google.appengine.runtime import apiproxy_errors from googleapiclient import discovery +from google.auth.transport import requests +import google.auth class Error(Exception): @@ -174,7 +176,7 @@ def get_modules(): appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] parent = 'apps/' + project - client = discovery.build('appengine', 'v1') + client = create_regional_admin_client() request = client.apps().services().list(appsId=project) response = request.execute() @@ -510,3 +512,68 @@ def get_hostname( hostname_parts.append(default_hostname) return ".".join(hostname_parts) + + +def get_current_region(): + """ + Dynamically determines the current GCP region by querying the metadata service. + This is the most reliable way to get the region from within a GCP environment. + + Returns: + str: The GCP region (e.g., 'us-central1'), or None if not found. + """ + try: + # google-auth can automatically fetch the project ID and other metadata + # in a GCP environment. We can get the region from the instance metadata. + scoped_credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/cloud-platform']) + + # The request object is needed to make authenticated calls + authed_session = requests.AuthorizedSession(scoped_credentials) + + # The metadata server URL for the instance's zone + metadata_url = 'http://metadata.google.internal/computeMetadata/v1/instance/zone' + metadata_response = authed_session.get( + metadata_url, + headers={'Metadata-Flavor': 'Google'} + ) + metadata_response.raise_for_status() # Raises an HTTPError for bad responses + + # The response is in the format 'projects/PROJECT_NUM/zones/ZONE'. + # We extract the zone (e.g., 'us-central1-a'). + zone = metadata_response.text.split('/')[-1] + + # The region is the zone without the final letter. + return zone[:-2] + + except Exception: + return None + +def create_regional_admin_client(): + """ + Creates an App Engine Admin API client configured for the region + where the code is currently running. + + Returns: + A Google API client resource object for the current region, or a global + client if the region cannot be determined. + """ + region = get_current_region() + + if not region: + print("Warning: Could not determine region. Falling back to global endpoint.") + return discovery.build('appengine', 'v1') + + print(f"Detected region: {region}. Creating regional client.") + + # The regional endpoint format for the App Engine Admin API + regional_endpoint = f'https://{region}-appengine.googleapis.com' + + # Build the service object, passing the regional endpoint URL + admin_api_client = discovery.build( + 'appengine', + 'v1', + static_discovery=False, + discoveryServiceUrl=f'{regional_endpoint}/$discovery/rest?version=v1' + ) + return admin_api_client From b15c605935e66246f01669c07240c3212f592104 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 16 Oct 2025 13:57:44 +0000 Subject: [PATCH 21/57] Using region specific client for admin api --- src/google/appengine/api/modules/modules.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index f8fc53e..f0d50de 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -573,7 +573,6 @@ def create_regional_admin_client(): admin_api_client = discovery.build( 'appengine', 'v1', - static_discovery=False, discoveryServiceUrl=f'{regional_endpoint}/$discovery/rest?version=v1' ) return admin_api_client From 40550409893ba41d5527911e23e08637d81def87 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 16 Oct 2025 14:16:41 +0000 Subject: [PATCH 22/57] Using region specific client for admin api --- src/google/appengine/api/modules/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index f0d50de..f8fc53e 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -573,6 +573,7 @@ def create_regional_admin_client(): admin_api_client = discovery.build( 'appengine', 'v1', + static_discovery=False, discoveryServiceUrl=f'{regional_endpoint}/$discovery/rest?version=v1' ) return admin_api_client From 5b9e41e2d005366f2697b4965171270caf741a81 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 17 Oct 2025 06:13:25 +0000 Subject: [PATCH 23/57] Using region specific client for admin api --- src/google/appengine/api/modules/modules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index f8fc53e..411c23b 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -57,6 +57,7 @@ from googleapiclient import discovery from google.auth.transport import requests import google.auth +from google.api_core.client_options import ClientOptions class Error(Exception): @@ -568,12 +569,13 @@ def create_regional_admin_client(): # The regional endpoint format for the App Engine Admin API regional_endpoint = f'https://{region}-appengine.googleapis.com' + + client_opts = ClientOptions(api_endpoint=regional_api_endpoint) # Build the service object, passing the regional endpoint URL admin_api_client = discovery.build( 'appengine', 'v1', - static_discovery=False, - discoveryServiceUrl=f'{regional_endpoint}/$discovery/rest?version=v1' + client_options=client_opts ) return admin_api_client From 35699c839dbc4a6433144e80ea0fa3d1abb3c82f Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 17 Oct 2025 06:21:24 +0000 Subject: [PATCH 24/57] Using region specific client for admin api --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 411c23b..57c24c4 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -570,7 +570,7 @@ def create_regional_admin_client(): # The regional endpoint format for the App Engine Admin API regional_endpoint = f'https://{region}-appengine.googleapis.com' - client_opts = ClientOptions(api_endpoint=regional_api_endpoint) + client_opts = ClientOptions(api_endpoint=regional_endpoint) # Build the service object, passing the regional endpoint URL admin_api_client = discovery.build( From a7f1b61a94331d7fea560ca259eaa33ba6997969 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 17 Oct 2025 07:12:39 +0000 Subject: [PATCH 25/57] Using region specific client for admin api --- src/google/appengine/api/modules/modules.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 57c24c4..e614132 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -57,6 +57,7 @@ from googleapiclient import discovery from google.auth.transport import requests import google.auth +from google.appengine.v1 import AppEngineAdminClient from google.api_core.client_options import ClientOptions @@ -177,11 +178,11 @@ def get_modules(): appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] parent = 'apps/' + project - client = create_regional_admin_client() - request = client.apps().services().list(appsId=project) - response = request.execute() + admin_client = create_regional_admin_client() + request = ListServicesRequest(parent=parent) + response = admin_client.list_services(request=request) - return [service['id'] for service in response.get('services', [])] + return [service.id for service in response] @@ -563,7 +564,7 @@ def create_regional_admin_client(): if not region: print("Warning: Could not determine region. Falling back to global endpoint.") - return discovery.build('appengine', 'v1') + return AppEngineAdminClient() print(f"Detected region: {region}. Creating regional client.") @@ -573,9 +574,5 @@ def create_regional_admin_client(): client_opts = ClientOptions(api_endpoint=regional_endpoint) # Build the service object, passing the regional endpoint URL - admin_api_client = discovery.build( - 'appengine', - 'v1', - client_options=client_opts - ) + admin_api_client = AppEngineAdminClient(client_options=client_options) return admin_api_client From b45a528fb059cb3893e4716793f16d726cb6545b Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 27 Oct 2025 11:30:14 +0000 Subject: [PATCH 26/57] setNumInstances implementation using adminAPI --- src/google/appengine/api/modules/modules.py | 42 +++++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index e614132..786817d 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -355,14 +355,40 @@ def _ResultHook(rpc): if not isinstance(instances, six.integer_types): raise TypeError("'instances' arg must be of type long or int.") - request = modules_service_pb2.SetNumInstancesRequest() - request.instances = instances - if module: - request.module = module - if version: - request.version = version - response = modules_service_pb2.SetNumInstancesResponse() - return _MakeAsyncCall('SetNumInstances', request, response, _ResultHook) + project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] + if module is None: + module = get_current_module_name() + if version is None: + version = get_current_version_name() + + client = discovery.build('appengine', 'v1') + + body = { + 'manualScaling': { + 'instances': instances + } + } + update_mask = 'manualScaling.instances' + rpc = apiproxy_stub_map.UserRPC('modules') + + def run_request(): + try: + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() + rpc.set_result(None) + except Exception as e: + # A complete implementation would map exceptions from the Admin API + # to the specific error types of the modules API. + rpc.set_error(e) + + thread = threading.Thread(target=run_request) + thread.start() + + return rpc def start_version(module, version): From d6bd3571fada6f577e9e8812d3295bf9668e8ee1 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 27 Oct 2025 12:02:12 +0000 Subject: [PATCH 27/57] fixing imports --- src/google/appengine/api/modules/modules.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 786817d..4434657 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -57,8 +57,6 @@ from googleapiclient import discovery from google.auth.transport import requests import google.auth -from google.appengine.v1 import AppEngineAdminClient -from google.api_core.client_options import ClientOptions class Error(Exception): @@ -178,11 +176,11 @@ def get_modules(): appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] parent = 'apps/' + project - admin_client = create_regional_admin_client() - request = ListServicesRequest(parent=parent) - response = admin_client.list_services(request=request) + client = discovery.build('appengine', 'v1') + request = client.apps().services().list(appsId=project) + response = request.execute() - return [service.id for service in response] + return [service['id'] for service in response.get('services', [])] From 88998aef4c2cb111907d000ca9e697b0a132c486 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 27 Oct 2025 12:12:11 +0000 Subject: [PATCH 28/57] fixing imports --- src/google/appengine/api/modules/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 4434657..ea22495 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -48,6 +48,7 @@ import logging import os +import threading import six From 2dcece307e1d16c817e71c98c0bcb542478c7582 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 27 Oct 2025 12:25:14 +0000 Subject: [PATCH 29/57] fixing imports --- src/google/appengine/api/modules/modules.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index ea22495..4457fa0 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -371,18 +371,13 @@ def _ResultHook(rpc): rpc = apiproxy_stub_map.UserRPC('modules') def run_request(): - try: - client.apps().services().versions().patch( - appsId=project_id, - servicesId=module, - versionsId=version, - updateMask=update_mask, - body=body).execute() - rpc.set_result(None) - except Exception as e: - # A complete implementation would map exceptions from the Admin API - # to the specific error types of the modules API. - rpc.set_error(e) + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() + rpc.set_result(None) thread = threading.Thread(target=run_request) thread.start() From 62ab9b245192bdaf4ca84f1ceb14196d8683ac4e Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 27 Oct 2025 14:55:04 +0000 Subject: [PATCH 30/57] fixing imports --- src/google/appengine/api/modules/modules.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 4457fa0..25278e9 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -377,7 +377,6 @@ def run_request(): versionsId=version, updateMask=update_mask, body=body).execute() - rpc.set_result(None) thread = threading.Thread(target=run_request) thread.start() From 4d25147e3fa0728cccdde65b0b069b999ce9c20b Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 27 Oct 2025 16:09:05 +0000 Subject: [PATCH 31/57] fixing imports --- src/google/appengine/api/modules/modules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 25278e9..947a79e 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -325,7 +325,8 @@ def set_num_instances( `TypeError` if the given instances type is invalid. """ rpc = set_num_instances_async(instances, module, version) - rpc.get_result() + rpc.wait() + rpc.check_success() def set_num_instances_async( From 68d88744067e5f75e744567f94f8fb7c4fdaf14e Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 28 Oct 2025 05:19:41 +0000 Subject: [PATCH 32/57] fixing imports --- src/google/appengine/api/modules/modules.py | 102 +++++++++++++------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 947a79e..5f58b88 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -346,43 +346,71 @@ def set_num_instances_async( A `UserRPC` to set the number of instances on the module version. """ - def _ResultHook(rpc): - mapped_errors = [ - modules_service_pb2.ModulesServiceError.INVALID_VERSION, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR - ] - _CheckAsyncResult(rpc, mapped_errors, {}) - - if not isinstance(instances, six.integer_types): - raise TypeError("'instances' arg must be of type long or int.") - project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] - if module is None: - module = get_current_module_name() - if version is None: - version = get_current_version_name() - - client = discovery.build('appengine', 'v1') - - body = { - 'manualScaling': { - 'instances': instances - } - } - update_mask = 'manualScaling.instances' - rpc = apiproxy_stub_map.UserRPC('modules') - - def run_request(): - client.apps().services().versions().patch( - appsId=project_id, - servicesId=module, - versionsId=version, - updateMask=update_mask, - body=body).execute() - - thread = threading.Thread(target=run_request) - thread.start() - - return rpc + class _ThreadedRpc: + """A class to emulate the UserRPC object for threaded operations.""" + + def __init__(self, target): + self.thread = threading.Thread(target=self._run_target, args=(target,)) + self.exception = None + self.done = threading.Event() + self.thread.start() + + def _run_target(self, target): + try: + target() + except Exception as e: + self.exception = e + finally: + self.done.set() + + def wait(self): + self.done.wait() + + def check_success(self): + if self.exception: + # Re-raise the exception caught in the thread + raise self.exception + + if not isinstance(instances, six.integer_types): + raise TypeError("'instances' arg must be of type long or int.") + + project_id = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if project_id is None: + appId = os.environ.get('GAE_APPLICATION') + project_id = appId.split('~', 1)[1]project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + if module is None: + module = get_current_module_name() + if version is None: + version = get_current_version_name() + + def run_request(): + """This function will be executed in a separate thread.""" + try: + client = discovery.build('appengine', 'v1') + body = { + 'manualScaling': { + 'instances': instances + } + } + update_mask = 'manualScaling.instances' + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() + except discovery.HttpError as e: + # Translate HTTP errors to the exceptions expected by the API + if e.resp.status == 400: + raise InvalidInstancesError(e) from e + elif e.resp.status == 404: + raise InvalidVersionError(e) from e + elif e.resp.status >= 500: + raise TransientError(e) from e + else: + raise Error(e) from e + + return _ThreadedRpc(target=run_request) def start_version(module, version): From 9ba8b3342d57f06bd3eb978413d0e42b441bff9b Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 28 Oct 2025 05:48:59 +0000 Subject: [PATCH 33/57] fixing imports --- src/google/appengine/api/modules/modules.py | 117 ++++++++++---------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 5f58b88..6d752fa 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -333,82 +333,79 @@ def set_num_instances_async( instances, module=None, version=None): - """Returns a `UserRPC` to set the number of instances on the module version. + """Returns a `UserRPC` to set the number of instances on the module version. - Args: - instances: The number of instances to set. - module: The module to set the number of instances for, if `None` the current - module will be used. - version: The version set the number of instances for, if `None` the current - version will be used. + Args: + instances: The number of instances to set. + module: The module to set the number of instances for, if `None` the current + module will be used. + version: The version set the number of instances for, if `None` the current + version will be used. - Returns: - A `UserRPC` to set the number of instances on the module version. - """ + Returns: + A `UserRPC` to set the number of instances on the module version. + """ - class _ThreadedRpc: + class _ThreadedRpc: """A class to emulate the UserRPC object for threaded operations.""" - def __init__(self, target): - self.thread = threading.Thread(target=self._run_target, args=(target,)) - self.exception = None - self.done = threading.Event() - self.thread.start() + def __init__(self, target): + self.thread = threading.Thread(target=self._run_target, args=(target,)) + self.exception = None + self.done = threading.Event() + self.thread.start() - def _run_target(self, target): - try: - target() - except Exception as e: - self.exception = e - finally: - self.done.set() + def _run_target(self, target): + try: + target() + except Exception as e: + self.exception = e + finally: + self.done.set() - def wait(self): - self.done.wait() + def wait(self): + self.done.wait() - def check_success(self): - if self.exception: - # Re-raise the exception caught in the thread - raise self.exception + def check_success(self): + if self.exception: + # Re-raise the exception caught in the thread + raise self.exception if not isinstance(instances, six.integer_types): - raise TypeError("'instances' arg must be of type long or int.") + raise TypeError("'instances' arg must be of type long or int.") - project_id = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') - if project_id is None: - appId = os.environ.get('GAE_APPLICATION') - project_id = appId.split('~', 1)[1]project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') + project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] if module is None: - module = get_current_module_name() + module = get_current_module_name() if version is None: - version = get_current_version_name() + version = get_current_version_name() def run_request(): - """This function will be executed in a separate thread.""" - try: - client = discovery.build('appengine', 'v1') - body = { - 'manualScaling': { - 'instances': instances + """This function will be executed in a separate thread.""" + try: + client = discovery.build('appengine', 'v1') + body = { + 'manualScaling': { + 'instances': instances + } } - } - update_mask = 'manualScaling.instances' - client.apps().services().versions().patch( - appsId=project_id, - servicesId=module, - versionsId=version, - updateMask=update_mask, - body=body).execute() - except discovery.HttpError as e: - # Translate HTTP errors to the exceptions expected by the API - if e.resp.status == 400: - raise InvalidInstancesError(e) from e - elif e.resp.status == 404: - raise InvalidVersionError(e) from e - elif e.resp.status >= 500: - raise TransientError(e) from e - else: - raise Error(e) from e + update_mask = 'manualScaling.instances' + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() + except discovery.HttpError as e: + # Translate HTTP errors to the exceptions expected by the API + if e.resp.status == 400: + raise InvalidInstancesError(e) from e + elif e.resp.status == 404: + raise InvalidVersionError(e) from e + elif e.resp.status >= 500: + raise TransientError(e) from e + else: + raise Error(e) from e return _ThreadedRpc(target=run_request) From f46eaec64aefa717d74eae6aa2fecc5eb03a1ce3 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 28 Oct 2025 06:54:37 +0000 Subject: [PATCH 34/57] Adding startModule and stopModule methods --- src/google/appengine/api/modules/modules.py | 139 ++++++++++++++------ 1 file changed, 96 insertions(+), 43 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 6d752fa..1a511d7 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -325,8 +325,7 @@ def set_num_instances( `TypeError` if the given instances type is invalid. """ rpc = set_num_instances_async(instances, module, version) - rpc.wait() - rpc.check_success() + rpc.get_result() def set_num_instances_async( @@ -363,11 +362,9 @@ def _run_target(self, target): finally: self.done.set() - def wait(self): - self.done.wait() - - def check_success(self): - if self.exception: + def get_result(self): + self.done.wait() + if self.exception: # Re-raise the exception caught in the thread raise self.exception @@ -438,23 +435,52 @@ def start_version_async( A `UserRPC` to start all instances for the given module version. """ - def _ResultHook(rpc): - mapped_errors = [ - modules_service_pb2.ModulesServiceError.INVALID_VERSION, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR - ] - expected_errors = { - modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE: - 'The specified module: %s, version: %s is already started.' % - (module, version) - } - _CheckAsyncResult(rpc, mapped_errors, expected_errors) - - request = modules_service_pb2.StartModuleRequest() - request.module = module - request.version = version - response = modules_service_pb2.StartModuleResponse() - return _MakeAsyncCall('StartModule', request, response, _ResultHook) + class _ThreadedRpc: + """A class to emulate the UserRPC object for threaded operations.""" + + def __init__(self, target): + self.thread = threading.Thread(target=self._run_target, args=(target,)) + self.exception = None + self.done = threading.Event() + self.thread.start() + + def _run_target(self, target): + try: + target() + except Exception as e: + self.exception = e + finally: + self.done.set() + + def get_result(self): + self.done.wait() + if self.exception: + # Re-raise the exception caught in the thread + raise self.exception + + project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] + def run_request(): + """This function will be executed in a separate thread.""" + try: + client = discovery.build('appengine', 'v1') + # To start a version, we patch its servingStatus to SERVING. + body = {'servingStatus': 'SERVING'} + update_mask = 'servingStatus' + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() + except discovery.HttpError as e: + if e.resp.status == 404: + raise InvalidVersionError(e) from e + elif e.resp.status >= 500: + raise TransientError(e) from e + else: + raise Error(e) from e + + return _ThreadedRpc(target=run_request) def stop_version( @@ -489,25 +515,52 @@ def stop_version_async( A `UserRPC` to stop all instances for the given module version. """ - def _ResultHook(rpc): - mapped_errors = [ - modules_service_pb2.ModulesServiceError.INVALID_VERSION, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR - ] - expected_errors = { - modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE: - 'The specified module: %s, version: %s is already stopped.' % - (module, version) - } - _CheckAsyncResult(rpc, mapped_errors, expected_errors) - - request = modules_service_pb2.StopModuleRequest() - if module: - request.module = module - if version: - request.version = version - response = modules_service_pb2.StopModuleResponse() - return _MakeAsyncCall('StopModule', request, response, _ResultHook) + class _ThreadedRpc: + """A class to emulate the UserRPC object for threaded operations.""" + + def __init__(self, target): + self.thread = threading.Thread(target=self._run_target, args=(target,)) + self.exception = None + self.done = threading.Event() + self.thread.start() + + def _run_target(self, target): + try: + target() + except Exception as e: + self.exception = e + finally: + self.done.set() + + def get_result(self): + self.done.wait() + if self.exception: + # Re-raise the exception caught in the thread + raise self.exception + + project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] + def run_request(): + """This function will be executed in a separate thread.""" + try: + client = discovery.build('appengine', 'v1') + # To start a version, we patch its servingStatus to SERVING. + body = {'servingStatus': 'STOPPED'} + update_mask = 'servingStatus' + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() + except discovery.HttpError as e: + if e.resp.status == 404: + raise InvalidVersionError(e) from e + elif e.resp.status >= 500: + raise TransientError(e) from e + else: + raise Error(e) from e + + return _ThreadedRpc(target=run_request) def get_hostname( From dd39515bd6f7e2d48b45f11b061ccf269a285d0d Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Tue, 28 Oct 2025 07:21:47 +0000 Subject: [PATCH 35/57] Adding startModule and stopModule methods --- src/google/appengine/api/modules/modules.py | 84 +++++++++++---------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 1a511d7..1eba4a2 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -422,18 +422,16 @@ def start_version(module, version): rpc.get_result() -def start_version_async( - module, - version): - """Returns a `UserRPC` to start all instances for the given module version. +def start_version_async(module, version): + """Returns a `UserRPC` to start the module version. - Args: - module: String containing the name of the module to affect. - version: String containing the name of the version of the module to start. + Args: + module: The module to start. + version: The version to start. - Returns: - A `UserRPC` to start all instances for the given module version. - """ + Returns: + A `UserRPC` to start the module version. + """ class _ThreadedRpc: """A class to emulate the UserRPC object for threaded operations.""" @@ -452,18 +450,24 @@ def _run_target(self, target): finally: self.done.set() - def get_result(self): - self.done.wait() - if self.exception: - # Re-raise the exception caught in the thread + def wait(self): + self.done.wait() + + def check_success(self): + if self.exception: raise self.exception - - project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] - def run_request(): + + def get_result(self): + self.wait() + self.check_success() + return None + + project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] + + def run_request(): """This function will be executed in a separate thread.""" try: client = discovery.build('appengine', 'v1') - # To start a version, we patch its servingStatus to SERVING. body = {'servingStatus': 'SERVING'} update_mask = 'servingStatus' client.apps().services().versions().patch( @@ -501,21 +505,17 @@ def stop_version( rpc.get_result() -def stop_version_async( - module=None, - version=None): - """Returns a `UserRPC` to stop all instances for the given module version. +def stop_version_async(module, version): + """Returns a `UserRPC` to stop the module version. - Args: - module: The module to affect, if `None` the current module is used. - version: The version of the given module to affect, if `None` the current - version is used. - - Returns: - A `UserRPC` to stop all instances for the given module version. - """ + Args: + module: The module to stop. + version: The version to stop. - class _ThreadedRpc: + Returns: + A `UserRPC` to stop the module version. + """ + class _ThreadedRpc: """A class to emulate the UserRPC object for threaded operations.""" def __init__(self, target): @@ -532,18 +532,24 @@ def _run_target(self, target): finally: self.done.set() - def get_result(self): - self.done.wait() - if self.exception: - # Re-raise the exception caught in the thread + def wait(self): + self.done.wait() + + def check_success(self): + if self.exception: raise self.exception - - project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] - def run_request(): + + def get_result(self): + self.wait() + self.check_success() + return None + + project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] + + def run_request(): """This function will be executed in a separate thread.""" try: client = discovery.build('appengine', 'v1') - # To start a version, we patch its servingStatus to SERVING. body = {'servingStatus': 'STOPPED'} update_mask = 'servingStatus' client.apps().services().versions().patch( From 3f86809c87597000e90984819ac7933e1b9c9bcc Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Sun, 9 Nov 2025 15:00:44 +0000 Subject: [PATCH 36/57] Add user agent for logging purpose --- src/google/appengine/api/modules/modules.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 1eba4a2..d00567b 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -177,7 +177,10 @@ def get_modules(): appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] parent = 'apps/' + project - client = discovery.build('appengine', 'v1') + creds, _ = google.auth.default() + http = requests.AuthorizedSession(creds) + http.headers.update({'User-Agent': 'appengine-sdk-modules-api/1.0'}) + client = discovery.build('appengine', 'v1', http=http) request = client.apps().services().list(appsId=project) response = request.execute() From 77eb02828b8cc516bf4bf3c87ae59b11e43e2c8c Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Sun, 9 Nov 2025 15:29:06 +0000 Subject: [PATCH 37/57] Add user agent for logging purpose --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1449bf5..59cf642 100644 --- a/setup.py +++ b/setup.py @@ -17,16 +17,18 @@ install_requires=[ "attrs>=21.2.0", "frozendict>=1.2", - "google-auth>=1.31.0", + "google-auth>=2.23.3", "mock>=4.0.3", "Pillow>=8.3.1", "protobuf>=3.19.0", "pytz>=2021.1", - "requests>=2.25.1", + "requests>=2.31.0", "ruamel.yaml>=0.17.7", "six>=1.15.0", "urllib3>=1.26.2,<2", "legacy-cgi>=2.6.2; python_version>='3.10'", + "google-auth-httplib2>=0.1.1", + "google-api-python-client>=2.92.0", ], classifiers=[ "Programming Language :: Python :: 3", From afab22738d033ca50187522c1058e53778a09939 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 10 Nov 2025 08:56:52 +0000 Subject: [PATCH 38/57] resetting setup.py --- setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 59cf642..1449bf5 100644 --- a/setup.py +++ b/setup.py @@ -17,18 +17,16 @@ install_requires=[ "attrs>=21.2.0", "frozendict>=1.2", - "google-auth>=2.23.3", + "google-auth>=1.31.0", "mock>=4.0.3", "Pillow>=8.3.1", "protobuf>=3.19.0", "pytz>=2021.1", - "requests>=2.31.0", + "requests>=2.25.1", "ruamel.yaml>=0.17.7", "six>=1.15.0", "urllib3>=1.26.2,<2", "legacy-cgi>=2.6.2; python_version>='3.10'", - "google-auth-httplib2>=0.1.1", - "google-api-python-client>=2.92.0", ], classifiers=[ "Programming Language :: Python :: 3", From de1e51b136d9abd59374e9aa44c892b6f2b1d027 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 10 Nov 2025 09:11:38 +0000 Subject: [PATCH 39/57] reverting --- src/google/appengine/api/modules/modules.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index d00567b..1eba4a2 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -177,10 +177,7 @@ def get_modules(): appId = os.environ.get('GAE_APPLICATION') project = appId.split('~', 1)[1] parent = 'apps/' + project - creds, _ = google.auth.default() - http = requests.AuthorizedSession(creds) - http.headers.update({'User-Agent': 'appengine-sdk-modules-api/1.0'}) - client = discovery.build('appengine', 'v1', http=http) + client = discovery.build('appengine', 'v1') request = client.apps().services().list(appsId=project) response = request.execute() From 7cb126e560c10351bc88cf575cdb0f589620b4d0 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Mon, 10 Nov 2025 09:24:46 +0000 Subject: [PATCH 40/57] adding http header --- src/google/appengine/api/modules/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 1eba4a2..29808c7 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -179,6 +179,7 @@ def get_modules(): parent = 'apps/' + project client = discovery.build('appengine', 'v1') request = client.apps().services().list(appsId=project) + request.headers['X-Goog-Api-Client'] = 'appengine-modules-api-python-client' response = request.execute() return [service['id'] for service in response.get('services', [])] From 64018013e66077d1b493d668ee7644af0fe178d5 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 12 Nov 2025 10:23:58 +0000 Subject: [PATCH 41/57] Adding tests --- .../appengine/api/modules/modules_test.py | 933 ++++++++---------- 1 file changed, 409 insertions(+), 524 deletions(-) diff --git a/tests/google/appengine/api/modules/modules_test.py b/tests/google/appengine/api/modules/modules_test.py index 2c2209a..34e8ddb 100755 --- a/tests/google/appengine/api/modules/modules_test.py +++ b/tests/google/appengine/api/modules/modules_test.py @@ -6,7 +6,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -16,18 +16,14 @@ # """Tests for google.appengine.api.modules.""" -import logging import os -import google - from google.appengine.api.modules import modules -from google.appengine.api.modules import modules_service_pb2 -from google.appengine.runtime import apiproxy_errors from google.appengine.runtime.context import ctx_test_util import mox from absl.testing import absltest +from googleapiclient import errors @ctx_test_util.isolated_context() @@ -36,564 +32,453 @@ class ModulesTest(absltest.TestCase): def setUp(self): """Setup testing environment.""" self.mox = mox.Mox() + self.mock_admin_api_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, 'discovery') + + # Environment variables are cleared in tearDown + os.environ['GAE_APPLICATION'] = 's~project' + os.environ['GOOGLE_CLOUD_PROJECT'] = 'project' + os.environ['GAE_SERVICE'] = 'default' + os.environ['GAE_VERSION'] = 'v1' + os.environ['CURRENT_MODULE_ID'] = 'default' + os.environ['CURRENT_VERSION_ID'] = 'v1.123' def tearDown(self): """Tear down testing environment.""" - self.mox.VerifyAll() self.mox.UnsetStubs() + self.mox.VerifyAll() - def testGetCurrentModuleName_DefaultModule(self): - """Test get_current_module_name for default engine.""" - os.environ['CURRENT_MODULE_ID'] = 'default' - os.environ['CURRENT_VERSION_ID'] = 'v1.123' - self.assertEqual('default', modules.get_current_module_name()) + # Clear environment variables that were set in tests + for var in [ + 'GAE_SERVICE', 'GAE_VERSION', 'CURRENT_MODULE_ID', 'CURRENT_VERSION_ID', + 'INSTANCE_ID', 'GAE_INSTANCE', 'GOOGLE_CLOUD_PROJECT', 'GAE_APPLICATION' + ]: + if var in os.environ: + del os.environ[var] - def testGetCurrentModuleName_NonDefaultModule(self): - """Test get_current_module_name for a non default engine.""" - os.environ['CURRENT_MODULE_ID'] = 'module1' - os.environ['CURRENT_VERSION_ID'] = 'v1.123' - self.assertEqual('module1', modules.get_current_module_name()) + def _SetupAdminApiMocks(self, project='project'): + modules.discovery.build('appengine', + 'v1').AndReturn(self.mock_admin_api_client) + + def _CreateHttpError(self, status, reason='Error'): + resp = self.mox.CreateMockAnything() + resp.status = status + resp.reason = reason + return errors.HttpError(resp, b'') - def testGetCurrentModuleName_GaeService(self): - """Test get_current_module_name from GAE_SERVICE.""" + # --- Tests for Get/Set Current Module, Version, Instance --- + + def testGetCurrentModuleName(self): os.environ['GAE_SERVICE'] = 'module1' - os.environ['GAE_VERSION'] = 'v1' self.assertEqual('module1', modules.get_current_module_name()) - def testGetCurrentVersionName_DefaultModule(self): - """Test get_current_version_name for default engine.""" - os.environ['CURRENT_VERSION_ID'] = 'v1.123' - self.assertEqual('v1', modules.get_current_version_name()) - - def testGetCurrentVersionName_NonDefaultModule(self): - """Test get_current_version_name for a non default engine.""" - os.environ['CURRENT_MODULE_ID'] = 'module1' - os.environ['CURRENT_VERSION_ID'] = 'v1.123' - self.assertEqual('v1', modules.get_current_version_name()) + def testGetCurrentModuleName_Fallback(self): + if 'GAE_SERVICE' in os.environ: + del os.environ['GAE_SERVICE'] + os.environ['CURRENT_MODULE_ID'] = 'module2' + self.assertEqual('module2', modules.get_current_module_name()) - def testGetCurrentVersionName_VersionIdContainsNone(self): - """Test get_current_version_name when 'None' is in version id.""" - os.environ['CURRENT_MODULE_ID'] = 'module1' - os.environ['CURRENT_VERSION_ID'] = 'None.123' - self.assertEqual(None, modules.get_current_version_name()) + def testGetCurrentVersionName(self): + os.environ['GAE_VERSION'] = 'v2' + self.assertEqual('v2', modules.get_current_version_name()) - def testGetCurrentVersionName_GaeVersion(self): - """Test get_current_module_name from GAE_SERVICE.""" - os.environ['GAE_SERVICE'] = 'module1' - os.environ['GAE_VERSION'] = 'v1' - self.assertEqual('v1', modules.get_current_version_name()) + def testGetCurrentVersionName_Fallback(self): + if 'GAE_VERSION' in os.environ: + del os.environ['GAE_VERSION'] + os.environ['CURRENT_VERSION_ID'] = 'v3.456' + self.assertEqual('v3', modules.get_current_version_name()) - def testGetCurrentInstanceId_Empty(self): - """Test get_current_instance_id when none has been set in the environ.""" - self.assertEqual(None, modules.get_current_instance_id()) + def testGetCurrentVersionName_None(self): + if 'GAE_VERSION' in os.environ: + del os.environ['GAE_VERSION'] + os.environ['CURRENT_VERSION_ID'] = 'None.456' + self.assertIsNone(modules.get_current_version_name()) def testGetCurrentInstanceId(self): - """Test get_current_instance_id.""" - os.environ['INSTANCE_ID'] = '123' - self.assertEqual('123', modules.get_current_instance_id()) - - def testGetCurrentInstanceId_GaeInstance(self): - """Test get_current_instance_id.""" - os.environ['GAE_INSTANCE'] = '123' - self.assertEqual('123', modules.get_current_instance_id()) - - def SetSuccessExpectations(self, method, expected_request, service_response): - rpc = MockRpc(method, expected_request, service_response) - self.mox.StubOutWithMock(modules, '_GetRpc') - modules._GetRpc().AndReturn(rpc) - self.mox.ReplayAll() + os.environ['GAE_INSTANCE'] = 'instance1' + self.assertEqual('instance1', modules.get_current_instance_id()) - def SetExceptionExpectations(self, method, expected_request, - application_error_number): - rpc = MockRpc(method, expected_request, None, application_error_number) - self.mox.StubOutWithMock(modules, '_GetRpc') - modules._GetRpc().AndReturn(rpc) - self.mox.ReplayAll() + def testGetCurrentInstanceId_Fallback(self): + if 'GAE_INSTANCE' in os.environ: + del os.environ['GAE_INSTANCE'] + os.environ['INSTANCE_ID'] = 'instance2' + self.assertEqual('instance2', modules.get_current_instance_id()) + + def testGetCurrentInstanceId_None(self): + if 'GAE_INSTANCE' in os.environ: + del os.environ['GAE_INSTANCE'] + if 'INSTANCE_ID' in os.environ: + del os.environ['INSTANCE_ID'] + self.assertIsNone(modules.get_current_instance_id()) + + # --- Tests for get_modules --- def testGetModules(self): - """Test we return the expected results.""" - service_response = modules_service_pb2.GetModulesResponse() - service_response.module.append('module1') - service_response.module.append('module2') - self.SetSuccessExpectations('GetModules', - modules_service_pb2.GetModulesRequest(), - service_response) - self.assertEqual(['module1', 'module2'], modules.get_modules()) + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.list(appsId='project').AndReturn(mock_request) + mock_request.execute().AndReturn( + {'services': [{'id': 'module1'}, {'id': 'default'}]}) + self.mox.ReplayAll() + self.assertEqual(['module1', 'default'], modules.get_modules()) + + def testGetModules_InvalidProject(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.list(appsId='project').AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaisesRegex(modules.Error, "Project 'project' not found."): + modules.get_modules() + + # --- Tests for get_versions --- def testGetVersions(self): - """Test we return the expected results.""" - expected_request = modules_service_pb2.GetVersionsRequest() - expected_request.module = 'module1' - service_response = modules_service_pb2.GetVersionsResponse() - service_response.version.append('v1') - service_response.version.append('v2') - self.SetSuccessExpectations('GetVersions', - expected_request, - service_response) - self.assertEqual(['v1', 'v2'], modules.get_versions('module1')) - - def testGetVersions_NoModule(self): - """Test we return the expected results when no module is passed.""" - expected_request = modules_service_pb2.GetVersionsRequest() - service_response = modules_service_pb2.GetVersionsResponse() - service_response.version.append('v1') - service_response.version.append('v2') - self.SetSuccessExpectations('GetVersions', - expected_request, - service_response) + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.list( + appsId='project', servicesId='default', view='FULL').AndReturn( + mock_request) + mock_request.execute().AndReturn({'versions': [{'id': 'v1'}, {'id': 'v2'}]}) + self.mox.ReplayAll() self.assertEqual(['v1', 'v2'], modules.get_versions()) - def testGetVersions_InvalidModuleError(self): - """Test we raise the right error when the given module is invalid.""" - self.SetExceptionExpectations( - 'GetVersions', modules_service_pb2.GetVersionsRequest(), - modules_service_pb2.ModulesServiceError.INVALID_MODULE) - self.assertRaises(modules.InvalidModuleError, modules.get_versions) + def testGetVersions_InvalidModule(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.list( + appsId='project', servicesId='foo', view='FULL').AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaisesRegex(modules.InvalidModuleError, + "Module 'foo' not found."): + modules.get_versions(module='foo') - def testGetVersions_TransientError(self): - """Test we raise the right error when a transient error is encountered.""" - self.SetExceptionExpectations( - 'GetVersions', modules_service_pb2.GetVersionsRequest(), - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) - self.assertRaises(modules.TransientError, modules.get_versions) + # --- Tests for get_default_version --- def testGetDefaultVersion(self): - """Test we return the expected results.""" - expected_request = modules_service_pb2.GetDefaultVersionRequest() - expected_request.module = 'module1' - service_response = modules_service_pb2.GetDefaultVersionResponse() - service_response.version = 'v1' - self.SetSuccessExpectations('GetDefaultVersion', - expected_request, - service_response) - self.assertEqual('v1', modules.get_default_version('module1')) - - def testGetDefaultVersion_NoModule(self): - """Test we return the expected results when no module is passed.""" - expected_request = modules_service_pb2.GetDefaultVersionRequest() - service_response = modules_service_pb2.GetDefaultVersionResponse() - service_response.version = 'v1' - self.SetSuccessExpectations('GetDefaultVersion', - expected_request, - service_response) + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.get(appsId='project', + servicesId='default').AndReturn(mock_request) + mock_request.execute().AndReturn( + {'split': {'allocations': {'v1': 0.5, 'v2': 0.5}}}) + self.mox.ReplayAll() self.assertEqual('v1', modules.get_default_version()) - def testGetDefaultVersion_InvalidModuleError(self): - """Test we raise an error when one is received from the lower API.""" - self.SetExceptionExpectations( - 'GetDefaultVersion', modules_service_pb2.GetDefaultVersionRequest(), - modules_service_pb2.ModulesServiceError.INVALID_MODULE) - self.assertRaises(modules.InvalidModuleError, modules.get_default_version) + def testGetDefaultVersion_Lexicographical(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.get(appsId='project', + servicesId='default').AndReturn(mock_request) + mock_request.execute().AndReturn( + {'split': {'allocations': {'v2-beta': 0.5, 'v1-stable': 0.5}}}) + self.mox.ReplayAll() + self.assertEqual('v1-stable', modules.get_default_version()) + + def testGetDefaultVersion_NoDefaultVersion(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.get(appsId='project', + servicesId='default').AndReturn(mock_request) + mock_request.execute().AndReturn({}) # No split allocations + self.mox.ReplayAll() + with self.assertRaisesRegex(modules.InvalidVersionError, + 'Could not determine default version'): + modules.get_default_version() + + def testGetDefaultVersion_InvalidModule(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.get(appsId='project', + servicesId='foo').AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaisesRegex(modules.InvalidModuleError, + "Module 'foo' not found."): + modules.get_default_version(module='foo') - def testGetDefaultVersion_InvalidVersionError(self): - """Test we raise an error when one is received from the lower API.""" - self.SetExceptionExpectations( - 'GetDefaultVersion', modules_service_pb2.GetDefaultVersionRequest(), - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - self.assertRaises(modules.InvalidVersionError, modules.get_default_version) + # --- Tests for get_num_instances --- def testGetNumInstances(self): - """Test we return the expected results.""" - expected_request = modules_service_pb2.GetNumInstancesRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - service_response = modules_service_pb2.GetNumInstancesResponse() - service_response.instances = 11 - self.SetSuccessExpectations('GetNumInstances', - expected_request, - service_response) - self.assertEqual(11, modules.get_num_instances('module1', 'v1')) - - def testGetNumInstances_NoVersion(self): - """Test we return the expected results when no version is passed.""" - expected_request = modules_service_pb2.GetNumInstancesRequest() - expected_request.module = 'module1' - service_response = modules_service_pb2.GetNumInstancesResponse() - service_response.instances = 11 - self.SetSuccessExpectations('GetNumInstances', - expected_request, - service_response) - self.assertEqual(11, modules.get_num_instances('module1')) - - def testGetNumInstances_NoModule(self): - """Test we return the expected results when no module is passed.""" - expected_request = modules_service_pb2.GetNumInstancesRequest() - expected_request.version = 'v1' - service_response = modules_service_pb2.GetNumInstancesResponse() - service_response.instances = 11 - self.SetSuccessExpectations('GetNumInstances', - expected_request, - service_response) - self.assertEqual(11, modules.get_num_instances(version='v1')) - - def testGetNumInstances_AllDefaults(self): - """Test we return the expected results when no args are passed.""" - expected_request = modules_service_pb2.GetNumInstancesRequest() - service_response = modules_service_pb2.GetNumInstancesResponse() - service_response.instances = 11 - self.SetSuccessExpectations('GetNumInstances', - expected_request, - service_response) - self.assertEqual(11, modules.get_num_instances()) - - def testGetNumInstances_InvalidVersionError(self): - """Test we raise the expected error when the API call fails.""" - expected_request = modules_service_pb2.GetNumInstancesRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'GetNumInstances', expected_request, - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - self.assertRaises(modules.InvalidVersionError, - modules.get_num_instances, 'module1', 'v1') + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.get(appsId='project', servicesId='default', + versionsId='v1').AndReturn(mock_request) + mock_request.execute().AndReturn({'manualScaling': {'instances': 5}}) + self.mox.ReplayAll() + self.assertEqual(5, modules.get_num_instances()) + + def testGetNumInstances_NoManualScaling(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.get(appsId='project', servicesId='default', + versionsId='v1').AndReturn(mock_request) + mock_request.execute().AndReturn({'automaticScaling': {}}) + self.mox.ReplayAll() + self.assertEqual(0, modules.get_num_instances()) + + def testGetNumInstances_InvalidVersion(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.get(appsId='project', servicesId='default', + versionsId='v-bad').AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaises(modules.InvalidVersionError): + modules.get_num_instances(version='v-bad') + + # --- Tests for async operations (set_num_instances, start/stop_version) --- def testSetNumInstances(self): - """Test we return the expected results.""" - expected_request = modules_service_pb2.SetNumInstancesRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - expected_request.instances = 12 - service_response = modules_service_pb2.SetNumInstancesResponse() - self.SetSuccessExpectations('SetNumInstances', - expected_request, - service_response) - modules.set_num_instances(12, 'module1', 'v1') - - def testSetNumInstances_NoVersion(self): - """Test we return the expected results when no version is passed.""" - expected_request = modules_service_pb2.SetNumInstancesRequest() - expected_request.module = 'module1' - expected_request.instances = 13 - service_response = modules_service_pb2.SetNumInstancesResponse() - self.SetSuccessExpectations('SetNumInstances', - expected_request, - service_response) - modules.set_num_instances(13, 'module1') - - def testSetNumInstances_NoModule(self): - """Test we return the expected results when no module is passed.""" - expected_request = modules_service_pb2.SetNumInstancesRequest() - expected_request.version = 'v1' - expected_request.instances = 14 - service_response = modules_service_pb2.SetNumInstancesResponse() - self.SetSuccessExpectations('SetNumInstances', - expected_request, - service_response) - modules.set_num_instances(14, version='v1') - - def testSetNumInstances_AllDefaults(self): - """Test we return the expected results when no args are passed.""" - expected_request = modules_service_pb2.SetNumInstancesRequest() - expected_request.instances = 15 - service_response = modules_service_pb2.SetNumInstancesResponse() - self.SetSuccessExpectations('SetNumInstances', - expected_request, - service_response) - modules.set_num_instances(15) - - def testSetNumInstances_BadInstancesType(self): - """Test we raise an error when we receive a bad instances type.""" - self.assertRaises(TypeError, modules.set_num_instances, 'no good') - - def testSetNumInstances_InvalidVersionError(self): - """Test we raise an error when we receive on from the underlying API.""" - expected_request = modules_service_pb2.SetNumInstancesRequest() - expected_request.instances = 23 - self.SetExceptionExpectations( - 'SetNumInstances', expected_request, - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - self.assertRaises(modules.InvalidVersionError, - modules.set_num_instances, 23) - - def testSetNumInstances_TransientError(self): - """Test we raise an error when we receive on from the underlying API.""" - expected_request = modules_service_pb2.SetNumInstancesRequest() - expected_request.instances = 23 - self.SetExceptionExpectations( - 'SetNumInstances', expected_request, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) - self.assertRaises(modules.TransientError, modules.set_num_instances, 23) + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='manualScaling.instances', + body={'manualScaling': {'instances': 10}}).AndReturn(mock_request) + mock_request.execute() + self.mox.ReplayAll() + modules.set_num_instances(10) + + def testSetNumInstances_TypeError(self): + with self.assertRaises(TypeError): + modules.set_num_instances('not-an-int') + + def testSetNumInstances_InvalidInstancesError(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='manualScaling.instances', + body={'manualScaling': {'instances': -1}}).AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(400)) + self.mox.ReplayAll() + with self.assertRaises(modules.InvalidInstancesError): + modules.set_num_instances(-1) def testStartVersion(self): - """Test we pass through the expected args.""" - expected_request = modules_service_pb2.StartModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - service_response = modules_service_pb2.StartModuleResponse() - self.SetSuccessExpectations('StartModule', - expected_request, - service_response) - modules.start_version('module1', 'v1') - - def testStartVersion_InvalidVersionError(self): - """Test we raise an error when we receive one from the API.""" - expected_request = modules_service_pb2.StartModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'StartModule', expected_request, - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - self.assertRaises(modules.InvalidVersionError, - modules.start_version, - 'module1', - 'v1') - - def testStartVersion_UnexpectedStateError(self): - """Test we don't raise an error if the version is already started.""" - expected_request = modules_service_pb2.StartModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.mox.StubOutWithMock(logging, 'info') - logging.info('The specified module: module1, version: v1 is already ' - 'started.') - self.SetExceptionExpectations( - 'StartModule', expected_request, - modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE) - modules.start_version('module1', 'v1') - - def testStartVersion_TransientError(self): - """Test we raise an error when we receive one from the API.""" - expected_request = modules_service_pb2.StartModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'StartModule', expected_request, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) - self.assertRaises(modules.TransientError, - modules.start_version, - 'module1', - 'v1') + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='servingStatus', + body={'servingStatus': 'SERVING'}).AndReturn(mock_request) + mock_request.execute() + self.mox.ReplayAll() + modules.start_version('default', 'v1') + + def testStartVersion_InvalidVersion(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v-bad', + updateMask='servingStatus', + body={'servingStatus': 'SERVING'}).AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaises(modules.InvalidVersionError): + modules.start_version('default', 'v-bad') + + def testStartVersionAsync_NoneArgs(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='servingStatus', + body={'servingStatus': 'SERVING'}).AndReturn(mock_request) + mock_request.execute() + self.mox.ReplayAll() + rpc = modules.start_version_async(None, None) + rpc.get_result() def testStopVersion(self): - """Test we pass through the expected args.""" - expected_request = modules_service_pb2.StopModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - service_response = modules_service_pb2.StopModuleResponse() - self.SetSuccessExpectations('StopModule', - expected_request, - service_response) - modules.stop_version('module1', 'v1') - - def testStopVersion_NoModule(self): - """Test we pass through the expected args.""" - expected_request = modules_service_pb2.StopModuleRequest() - expected_request.version = 'v1' - service_response = modules_service_pb2.StopModuleResponse() - self.SetSuccessExpectations('StopModule', - expected_request, - service_response) - modules.stop_version(version='v1') - - def testStopVersion_NoVersion(self): - """Test we pass through the expected args.""" - expected_request = modules_service_pb2.StopModuleRequest() - expected_request.module = 'module1' - service_response = modules_service_pb2.StopModuleResponse() - self.SetSuccessExpectations('StopModule', - expected_request, - service_response) - modules.stop_version('module1') - - def testStopVersion_InvalidVersionError(self): - """Test we raise an error when we receive one from the API.""" - expected_request = modules_service_pb2.StopModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'StopModule', expected_request, - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - self.assertRaises(modules.InvalidVersionError, - modules.stop_version, - 'module1', - 'v1') - - def testStopVersion_AlreadyStopped(self): - """Test we don't raise an error if the version is already stopped.""" - expected_request = modules_service_pb2.StopModuleRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.mox.StubOutWithMock(logging, 'info') - logging.info('The specified module: module1, version: v1 is already ' - 'stopped.') - self.SetExceptionExpectations( - 'StopModule', expected_request, - modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE) - modules.stop_version('module1', 'v1') + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='servingStatus', + body={'servingStatus': 'STOPPED'}).AndReturn(mock_request) + mock_request.execute() + self.mox.ReplayAll() + modules.stop_version() + + def testStopVersion_InvalidVersion(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v-bad', + updateMask='servingStatus', + body={'servingStatus': 'STOPPED'}).AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaises(modules.InvalidVersionError): + modules.stop_version(version='v-bad') def testStopVersion_TransientError(self): - """Test we raise an error when we receive one from the API.""" - self.SetExceptionExpectations( - 'StopModule', modules_service_pb2.StopModuleRequest(), - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) - self.assertRaises(modules.TransientError, modules.stop_version) + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='servingStatus', + body={'servingStatus': 'STOPPED'}).AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(500)) + self.mox.ReplayAll() + with self.assertRaises(modules.TransientError): + modules.stop_version() + + def testRaiseError_Generic(self): + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.patch( + appsId='project', + servicesId='default', + versionsId='v1', + updateMask='servingStatus', + body={'servingStatus': 'STOPPED'}).AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(401)) # Unauthorized + self.mox.ReplayAll() + with self.assertRaises(modules.Error): + modules.stop_version() + + # --- Tests for get_hostname (intentionally left incomplete) --- def testGetHostname(self): - """Test we pass through the expected args.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - expected_request.instance = '3' - service_response = modules_service_pb2.GetHostnameResponse() - service_response.hostname = 'abc' - self.SetSuccessExpectations('GetHostname', - expected_request, - service_response) - self.assertEqual('abc', modules.get_hostname('module1', 'v1', '3')) - - def testGetHostname_NoModule(self): - """Test we pass through the expected args when no module is specified.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.version = 'v1' - expected_request.instance = '3' - service_response = modules_service_pb2.GetHostnameResponse() - service_response.hostname = 'abc' - self.SetSuccessExpectations('GetHostname', - expected_request, - service_response) - self.assertEqual('abc', modules.get_hostname(version='v1', instance='3')) - - def testGetHostname_NoVersion(self): - """Test we pass through the expected args when no version is specified.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.instance = '3' - service_response = modules_service_pb2.GetHostnameResponse() - service_response.hostname = 'abc' - self.SetSuccessExpectations('GetHostname', - expected_request, - service_response) - self.assertEqual('abc', - modules.get_hostname(module='module1', instance='3')) - - def testGetHostname_IntInstance(self): - """Test we pass through the expected args when an int instance is given.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.instance = '3' - service_response = modules_service_pb2.GetHostnameResponse() - service_response.hostname = 'abc' - self.SetSuccessExpectations('GetHostname', - expected_request, - service_response) - self.assertEqual('abc', modules.get_hostname(module='module1', instance=3)) - - def testGetHostname_InstanceZero(self): - """Test we pass through the expected args when instance zero is given.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.instance = '0' - service_response = modules_service_pb2.GetHostnameResponse() - service_response.hostname = 'abc' - self.SetSuccessExpectations('GetHostname', - expected_request, - service_response) - self.assertEqual('abc', modules.get_hostname(module='module1', instance=0)) - - def testGetHostname_NoArgs(self): - """Test we pass through the expected args when none are given.""" - expected_request = modules_service_pb2.GetHostnameRequest() - service_response = modules_service_pb2.GetHostnameResponse() - service_response.hostname = 'abc' - self.SetSuccessExpectations('GetHostname', - expected_request, - service_response) - self.assertEqual('abc', modules.get_hostname()) - - def testGetHostname_BadInstanceType(self): - """Test get_hostname throws a TypeError when passed a float for instance.""" - self.assertRaises(TypeError, - modules.get_hostname, - 'module1', - 'v1', - 1.2) - - def testGetHostname_InvalidModuleError(self): - """Test we raise an error when we receive one from the API.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'GetHostname', expected_request, - modules_service_pb2.ModulesServiceError.INVALID_MODULE) - self.assertRaises(modules.InvalidModuleError, - modules.get_hostname, - 'module1', - 'v1') - - def testGetHostname_InvalidInstancesError(self): - """Test we raise an error when we receive one from the API.""" - self.SetExceptionExpectations( - 'GetHostname', modules_service_pb2.GetHostnameRequest(), - modules_service_pb2.ModulesServiceError.INVALID_INSTANCES) - self.assertRaises(modules.InvalidInstancesError, modules.get_hostname) - - def testGetHostname_UnKnownError(self): - """Test we raise an error when we receive one from the API.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'GetHostname', expected_request, 1099) - self.assertRaisesRegex(modules.Error, - 'ApplicationError: 1099', - modules.get_hostname, - 'module1', - 'v1') - - def testGetHostname_UnMappedError(self): - """Test we raise an error when we receive one from the API.""" - expected_request = modules_service_pb2.GetHostnameRequest() - expected_request.module = 'module1' - expected_request.version = 'v1' - self.SetExceptionExpectations( - 'GetHostname', expected_request, - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - expected_message = 'ApplicationError: %s' % ( - modules_service_pb2.ModulesServiceError.INVALID_VERSION) - self.assertRaisesRegex(modules.Error, - expected_message, - modules.get_hostname, - 'module1', - 'v1') - - -class MockRpc(object): - """Mock UserRPC class.""" - - def __init__(self, expected_method, expected_request, service_response=None, - application_error_number=None): - self._expected_method = expected_method - self._expected_request = expected_request - self._service_response = service_response - self._application_error_number = application_error_number - - def check_success(self): - self._check_success_called = True - if self._application_error_number is not None: - raise apiproxy_errors.ApplicationError(self._application_error_number) - self.response.CopyFrom(self._service_response) - - def get_result(self): - self._check_success_called = False - result = self._hook(self) - if not self._check_success_called: - raise AssertionError('The hook is expected to call check_success()') - return result - - def make_call(self, method, - request, response, get_result_hook=None, user_data=None): - self.method = method - if self._expected_method != method: - raise ValueError('expected method %s but got method %s' % - (self._expected_method, method)) - self.request = request - if self._expected_request != request: - raise ValueError('expected request %s but got request %s' % - (self._expected_request, request)) - self.response = response - self._hook = get_result_hook - self.user_data = user_data + # This test verifies the current non-error-handled behavior of get_hostname + self._SetupAdminApiMocks() + mock_apps = self.mox.CreateMockAnything() + mock_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.get(appsId='project').AndReturn(mock_request) + mock_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + self.mox.ReplayAll() + self.assertEqual('i.v1.default.project.appspot.com', + modules.get_hostname(instance='i')) if __name__ == '__main__': From 3928c0be88ceac3421f8e8a98ac2421ba1f22e74 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 13 Nov 2025 05:01:55 +0000 Subject: [PATCH 42/57] experimenting with http headers --- .coverage | Bin 0 -> 118784 bytes src/google/appengine/api/modules/modules.py | 623 ++++++++------------ tox.ini | 1 + 3 files changed, 238 insertions(+), 386 deletions(-) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..76b688eb0057e7411fd828b40870c05310e22520 GIT binary patch literal 118784 zcmeHw37lJ3eeRKTwQqJDV;jd_GOFE!*`S>5k@lBwfwDS2Lrr zXU1o;QMN)^3I&otNka>SLSG4mgrz)6OG|Cw-Yt~9#({^vXYv;UXtuDRTB^vZ;3wKS(PM%Iv6jO?#e2q9Vg--7?aBaI6s zc!Sqix>$Y{DWu*+%C{7+D(@`3w|IK~8--T(_4&KfTDFEi$cJP=G9Vd{ z4E*P2fL>EbZ`iacw!GkI<4xVMw3=>t&&7+bJ$S+O2P@ZKaN*?#E8b_7GjbI?cJ8cP zP_fL}N=LUU6Gl_7811@I(;TDSs5p~)@a?Xx*V#aa&oI=nFYa>hXIH9G$6b!zz)y56 zqorBB%9P&Q;eNqKbM!gK`v5(<5Y)ZH>^Rt^911Es+S{i0klfgh@wR-H*dZfWh>q;Bu1XkiI!CghxRywX}`tLm7Q zrqOnb*|rVGFxwS#A35YT|r@~I8yt&0(8r9Nj*H!$POq?`0;U6hyDHddc#`Dppvjw)NEGF3Af3xyDMJh zK%1j&rW%l;S*)Q|O#h3JbfntGgoO;1@uoRGsB5mq*g6^r`hwtV6Ww-=H5Hqcp=QqL zme$aB`G57gg;6UKRA)~%wc+5_^XJojd54xJ2$!P zx6tXc|A_&S*(ZIGFVlNFXS61Ila6LsXtL;oy%C_ozF61rWo_K-IwLt(MlV`{NZGhP_`!-_IH+!|^&1+*sL$ zMroso4*h2EqyAr6j}`p>t`0b1zuMC3UUO%a68Xj1Nqxo7d85$*&CwmBrB~QjTwz?r zn$_$|&C>BljHTPRC7wwQSZ%3$*fU!tXV_GZZjGfkyy!(S+VvZw*UzXn2KAN}i0!|P z4=BmdAN%@p6*^qZcA|R1xQo!i-g*eEbMtC&++!j5mOt0YZ7)K~SotY}fAS$2kPJu$ zBm7M?5vF}tQ3+F=AGtRB$q zDaY(owN6KGHxQO_XQ$^(n(dvogGg$cRj=AsEx14Y625L!Stvx?K^ViT8=;Q9RoQzM z>9Etg3Z+;lMv#_S?>2S2>c6VCZp|1qUG0qTg&@oXdCROqr@0ei>xgqVS#9X1+I5U3 zux|qPhIxg~a>Yit)+&PTsfgy<(5tFyv<*j90lx_N=O0yQFV0gJ7`&yoOcr+(Y7xOF zZ-W5qxeLJJ`gw{NUZL)75I{Z(0XB2509I^-D0K9e|AKbV9S8mufM3A>kv&R_3SHn@ z`_MK=x7z4rI+lsn->Lb^!07+FjQ=;Xs+Yi$0m*=5Kr$d1kPJu$Bm7LSZX$F6k56OUJKr$d1kPJu$BmybwSwl* zAHR(pd+4e-`P|pGeD1FaIrP|HKKU^D{8{9YhwbE{_@TewmHzxm#~%93lZcgn=&@ub z1+WtoLUQYqxZ06GA$DiUep1Ph2@sQ*R2bIPDHMc!yNL2Y%#Hg$i03u9ybWJDlv=xy zWF9=AkYgl%>=-^Fqyl0HIr?Ed9mDr)Z`^a}jeEQ|M2Wi}?k1!_4&gIAj>X7s<&*>j zxHt`j&au7U24IDKPLAzA_HOo_%>KBtKOe_;HsS~H@1kT($!1G2cK%-~-$M}l?}z2T zDnC;G2BQD{p!~)1gXOQ3f2Vw3`JVFq><8pSG9Vd{3`hnf1CjyBfMh^2AQ_MhNCqSW zk^z1UupNe2vE;rKitbCP;J)PZ?n^f3zGSlQOU~Vrh$YkRJ0<15B$Dn+N^xJ(3HK!) zSFi!#p8sPrLOvt|k^#wpWI!??8ITM}1|$QL0m*=5Kr$d1cn&kb&j025|2bU2vS^Y4 z$$(@)G9Vd{3`hnf1CjyBfMh^2AQ=Dy%>Q4m|D`HO1|$QL0m*=5Kr$d1kPJu$BmbPdROnhl1I^os`#9&1H zK20mjWzj^m&9-XSr&OcWF)hbUefpJ=P|?zD8wr423I1p#I^j#8I=zl3#vhENMjCn> z`IV6q*UP8ORO7#mq`T+~!t%2+Ilej)%?RXR68srekfB<1BY^~JkffGL@p~iD&$@2c zETe-|xqCbADDpKLSC*5a$>@v0sQ9lVl3{}7sy4hKt?e|ev8rR~ z`m8aJOzW&39zd?@t{_#22*98}&pl6fKUH)9ZBFTJcc}Qxf7?)8}e>hxIzH1P_fsg1YIj zc8>vxwY*RE6l#lA{`!Q*kRM!9=#p6E8fbUW23+nxD_-?#W~-w)tni;+ROo`(!bb<2 z-O@MIazq%qad4rB6KJ)qlDj@hYd zosQma7;Sy0H<{hZYOszWt7==d;QsJS)d-rm{=bft7s{m6Bi9xGxp;eVP2oL-)APTT zKPUI8+@9?H*-~a9{djsJ^{teVd?LB1JgKx2PbBR4|B1I_-;dowzBRD?AIa2l)@~S8 zTeoJ6n!CloGXra5k7rl?ozM$J*#Ta~@X3JV&1H7zL1n*|CHiUT3K zki_Fd?Tqh*G}p3zG%+%1Ai?!)p|^H45)4pr-4TWMMj;1CcqMwZCBfJcoxBqBm|21F zk-p{{npc)L@yFI`Qw_`Pw(F`hi78~gIzIdfs^NULph}NGm@S=o-qY8g;$x7*3Ck!r>dQ1NL`49k#Dj z7E$^Tf2A-W#iT`LdCMr}80dvdXLpm-K{L})21&_miLWNpt?Sxb(>Dj z(p{a>n)ZNvEw&6!j!r&MWU(!|qF52smhNbE&C%Qmjc!4VX>3O<2$ll7BZi}@K<{8Z z*cE_oU^8^C=XE75tpns#SD`Jj$UxoeD0E63s@16hG%X5Lb#%;@9is`*(}jVuhV4(x zw{;X+<4$j6-VqnMofd59SKTF=&olY&BiY(_MTihVzA12jBP1f=;EzXA4cORg+w2e$ z>Q27Q?EgFP&X{sCZG~PYSQi|3&^axj)a{nEg&x%{-P_$RyKmPM?sv zCpDJ*T=G)oQN>97L*hsx6@Od&l-S2&yYUkuAG0Sbgxb7U_t=1K4lSY=Dga*M4UFle z);7mYO||stF1ja-J?`v%dMn@?yy0uDjv8*mnJ)ljjW;CBB_?#IHVLJi-XfxmGg{NY zoWeWWvQGl!6z?+D4I4X~wMoA&?ad-UhZJb``2gkKaJtxe8frM^CIE9+qnc&5eR|s) z0jTk=Q-7fgwY4{hsLr0&YO3R1qgi+7!PfHtYVacIlEi)2F75S#R>{!CmOsrkz`dC_ zF6)fgVU|?}8uzy1l5371)@y8nMM+5^%SoO^w7S@R!pRWM?HnB2R%_0jyP^pyceaRP zbM5hmC1A5`lzp*)qH|r=3?47Brqi>A9ZbvGb>vS?@M= zJ9r%$bLX>QfNNJ4)Bt4h_A0_r!p;Dj>y#?Mj<9a0d4n?2!@_zS6>}+oc6sXA0H}Mi z=j{f~B!SGm-yYI2wCq_0xLmswR^k9RQomLLz}&m=iEgv0dYkzutQiL;_lawykW(=L za-CmAz}Wh~LKaE+j&iB=-qNYX`-|rlzErph>wi7>_qnB9A^WcEmdxFm?dgxF&rdy+ zIw$#Ha<}qv<%NlR66@mch?ir_O;=l58?snY3k;gu2N^brmtloejF803AWWXOFg2~CjT=o^mZDz5H{MP+ zq&WIgw15SnLPVMb&AmiKg@nZ(=3wBAjK*FpLPJ-akPm9;`638eDZ`3Spc@P4K{vQ> zz_s*NO{-1n)!-snq0#IwfQt>hbu)k+#2=d8&7BJ>xKBJnQKC&mIR`8hN5H~B<2Z6Q z2;e%yXTwLZH95q7_bh;OU#t83OMOF%nK8g}pYw%f4(IdEUf{NP*Esm@pLw#8U=L8a zj|AR6|4NIw-fp1J@-B0KU&OT|!Ek_rTU9`FpXs`r6=4-m?*bCoagwa4T z+dBcteUp|gnE(qsX9w`OYjN0m7%)(0UIbL`ZPr^aMNhLkX9Ab|<|z)Sy1KvV?rayq zudB9Ac=<9HKGs;y04n#!!e%3VJ34zCFuCq;u(1dKUDsLPQouf46llaIV)itEa$oI7 zdcnYEu=7Fyb64N6^Jfk9R3LJ1sQsd9^_J`6Gj|GbxzFSVPUZZriLL*)lHVoe*OfD+ z+e^9PTZ@|t?=PI0|0w+bKao2(`>E_$=KjoB`rhsW{j0Aq;D^*L9^mtG4MPw<}l;?#A@ojz2?juM~kDx-Iqnserw7y$BUt1v^y1)OCptV;mszjOyj%osV8h_Q0(bUe z;BsGW2e=Xa?i~a$_ZiOMS**VZrR0E!rZt2;(6kmWf&^TRmDp52DuHKVZ5dvCwwVh> zsGE-;sr6i3#rpr7gnTo0eg3+_rTK3a&n`TacvIrU_`9$tu>XZ#l5ZC+&Fmqe> zj+=HbAyOyzGDo{Vd%zIq0x_Pgg8~e;lmkN8ACka3A3A6$w85SIfrGbQLP8G=Iu1Oq ziNH48i^(uCk6J&iP+bUEFhPEykbcAKY6trGlM$$BntxFTO^rcuQcQt;+{g&42rO{) zJoQC1rz~&a&-ALsM?*vFF9-=E6B_>n7lE>$CngR+02{G`8c_Xngm~hIQbcs{Tufay ztvY>S@vrjSl_Z?;4Vw>oL(0p9_>@Q7?KTM+!v;K4w+iV7`tzmWwDwdXLraK@p7uoH z)3d?V=|w`AUb#BGP{=H0(B3~GBm~5eFYrl12o6;$^uSx|o&SG;ly57?O0<+N-cigK zZqGlRU(Tm{=N8IJQ4e)*oou= ztM-FGr4Ktx*cst!1;V`M+V$}&^M+)$cfCd4J7EkF!G~0YVrb~m-A-!(&|EE<7&OWn zgw#QC?W4ftYRTkdcFblEo60(yo|{Ji%GImJ2Sp@IHasCgN3zB!xf(>D~n`AtLFavXcN-Pnl2FDc#4UQ_)5P`cN#DEB^L_<5;fnIXA zk{^_!9}SHrO^{IL+0C)DlL7eXn_x*wOKr$;^62Ck4kt*rv>?#F(Fyeb2q#F>O~|*7 zw|s#$9~P!nJv1~^bwuFlYA|R(J6f88Al$9ZqJsEVl@oBvWN{jLhtYz#AAUv!aDvnd zM+`u7Kh%#xN5_}MB$(&!?-@n+nRm<2vMaD|G|><(Mcu4*TkyAmM=l~Vxl;5dMCyQN z&g%zYS<(gSf20j|0+Z&sI?B#s|5tGE0k_p8r*~K3gZ^aJ4n+qHBcjr&bJ&-$) z{aW_M%wK1w(@&;fmoCMgEd3<4RGuS`6Epc#atXU7M-orrN1ok-dq#(Fi=!G|VWv{o z=g`fNkI)R_&vQ-b5TaIVO|x59!}qEmaLyg?$cN(=NrN&WXO5vkZ*-EA(dW zi-sU@HbczF>~>~MjjkD|;x)0|><8;cYX_4)242FO8FeZ4(nAmlvTNrnJ6WzqGyno(GJuZ z%0laN^@t2mTSmR!)MqsW3vi3eLhK7EE+$BBTri+^DGRhOM6cc4=g1Q=&ysl_p)A%u z*MpjXX8+udc_Id5);wiV_qq0_%$8)(;Wr40-=i!FKiA{2LGZ{4)jZ`MkDp=tI{|DW z9s#E*iirGtLjVk+_XQakN#-bv(a-gSh6w`A!^BjtM@g8YEKI*R=s*UvxmjcgKi~L80c_OR@hD}1__-SB1hDnPFex?F{y>dVcX8~+dBhCh!>l4~e$Ym` zX94z)tPR2R27n!`sst%=U{-3w4@OXnjf_hj_?QSPXkf5wA)&ee(_~heekK#Qy|Uo@ zT;q)enyb%X4&O9QmSxk1H>#jYWme#<(E!!L#)u$>TXqA}X;xG?w2_VEZc@fbTUshU zRdfnJELi!+Vq3}GvG>F`#^0{2O}rzqF|#-Q+4Lo;M^cl?A0#d1UkjJzAIaBp-_P}O zsq9;@L-6av>;Iq1`z7Q`7j!)22H))sOuEP?M4rzD7D?WAB)Ok^YRdlBfnWBSvx5Dp z4+t!{-PStxr0E3Mzb^7%(ncaoZ5;6xJL9U}W&!w^K^plD?&Pz6C!Ct}uNe3{xRXB! z9vN83+qv&XGwcZBEP9S3Kb5}X-~S`rPs>M0>-`C3c%@=aIbiwvYwY+p#&rM;!J`LrX9YXy)c;D=liyFal^-b0 z#6Kpi_>baqv7f{i$iMyv=kH6DCBEb8^##*^N4MHqv#Mcp#~Q;iPt9z!ncETDpj@EH zcE>l0?g|O8xrI4n+ue9yq)2qf_rQ7uzCKseJIoY?5im=U>W=SO{R#o<6B>}`De~I! zJN?>3W2Kw zrjAshNM*-2MnWI@_@<6M%Cg&WwPERt5qcMUQ~r6%(%f-1-icsqINFC>J=@Y|t^O!w zIq!tGzF>L{6B=Z{<7-0GFZ_!7hxHcz>N;I^DNBdP)o`pYNTmBi`$twhQSE>A0}UFZ z$dSi4({;b_;g#111*)(}ku#6)@EamWR$5qj`{vF)%JSz4>H~(3wH7E#qbFz-3TfzX zJJ38uIz7HgWJ1`L&dZmD9mMIMDAfFXc))Am9%vn@sPEkVThDR(y(*z%G*!rh57 zaElpo_ag7*f0P(NukPZGp09Ff&`fq#0OVF7B+oNJgI>>!r7`{oZz z5ZOY>BOJk$5Vec=Rgzy7*2o4NXvzD~oAVIJA@AaDJr|TX zqEs2~0~fQNTgA7G{1X2-bPT%hN*K8uG;sfAw*FrO6WsFak?ZfS($?aKisu%-AjrD^ zXL8;wg^~sOKR_Jtm?p2|KK%}H`~LsrO^D#9 zb6s8qsDZl-xkDIaU$S(?OI{lCXAa{9^9^<`#0X>|3*&@Mrmu3`hnf1CjyB zfMh^2z?lL5$DJ(DqusL|z2(0QMFYP~NHlO?g2;^Qa_)gL+>*lyw`s66AUnCAHM)3S z_N#8&U=enLBjp=~WIu}Lo(nwy47b(M4pgB{av%rd zgxJ!1*!Dn1;c=|NTf!QHD*-saS!5UN?uPcwCN<{&za@4tDc@esl$J}Rc(m}Nf|GwD zKb8A#?w0IdWDjK?$-FxK_4GBVKTN$M`Q_xxln0fw6CX>A#XlQAC-!OFDj#H;-UK(2 z2G2~oE39pM^hVy!Ovkmhg;VDZ@Lb`V`1;vc+gkLs;DF`Mv8qiIwp-PPX*QaAb<%9% z$ugWtW2&JY)-A1iK)0tHv%~U8d*-`45r%luZ0}^@80v5kui92ExIg?-)e|FXZQG-- z5kvec*0wZGUk&23JWJg7;YN&5xK4Izt{G(2?<>(`!FlUdyq&j>Cq-nuT%xap6oLYX z407S;?WIY2Jw)J|`+OB55KI6Xjl^}H2BSt6>2;8U>(KQKa<~FqI|2bLykI|5zSAhnu8NFA*uj_WrGCJrj z_I6lpEYd4Spbn9~0qStY2-Kn9GFjINBIfDkPzSCKrmIwk-#snT%SKQOE#0=YhMOS< z{o|#OKy?4uMxrgdKBZ!XgYdr0fF7>4mp~A%uC}Yx1DqIK{|S=?e?ilH1!NH2H2vyO zonFVSlO_6cNFjREbS$lHv)XVwhl?SB=)ubEsmHq$UQHYvK}`e_s4N$&r<4aEhIpk6 z`fA9qL@yeFHn>b2w!LN%rx!v5(NkLV)T%3n7l?13ULdZY_Jh&E_QBFv?=(mE1DtDy zoqp%!gR`JifcDP+A0p*j5&zFZ{Qp~vYYXo!oSOe|en;*jxxLv3nE$_A;!6f31CjyB zfMh^2AQ|`>VL<5dY2@6pqgmJt#q3EJV3PzFePk$*yyE!jB*yoa-RTt&KRzVaaPk@}~QT^?m%H_%{gAwZKo>e}#^}oO{p( zMtgu8QSytqJB7QzI*vqb2kWB=wqLiIyQ>ic1!PzE5ziD-ucp>@CadlzYvoXxaoA8 Nx?S~OgNT@y{u@6w34Z_p literal 0 HcmV?d00001 diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 29808c7..58294f9 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -1,19 +1,3 @@ -#!/usr/bin/env python -# -# Copyright 2007 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# """Exposes methods to control services (modules) and versions of an app. Services were formerly known as modules and the API methods still @@ -22,6 +6,18 @@ https://cloud.google.com/appengine/docs/standard/python/using-the-modules-api. """ +import logging +import os +import threading + +from google.appengine.api import apiproxy_stub_map +from google.appengine.api.modules import modules_service_pb2 +from google.appengine.runtime import apiproxy_errors +from googleapiclient import discovery, errors, http +import six +import httplib2 + + __all__ = [ 'Error', 'InvalidModuleError', @@ -46,20 +42,6 @@ 'get_hostname' ] -import logging -import os -import threading - -import six - -from google.appengine.api import apiproxy_stub_map -from google.appengine.api.modules import modules_service_pb2 -from google.appengine.runtime import apiproxy_errors -from googleapiclient import discovery -from google.auth.transport import requests -import google.auth - - class Error(Exception): """Base-class for errors in this module.""" @@ -83,6 +65,25 @@ class UnexpectedStateError(Error): class TransientError(Error): """A transient error was encountered, retry the operation.""" +def _raise_error(e): + # Translate HTTP errors to the exceptions expected by the API + if e.resp.status == 400: + raise InvalidInstancesError(e) from e + elif e.resp.status == 404: + raise InvalidVersionError(e) from e + elif e.resp.status >= 500: + raise TransientError(e) from e + else: + raise Error(e) from e + +def _get_project_id(): + project_id = os.environ.get('GAE_PROJECT') or os.environ.get( + 'GOOGLE_CLOUD_PROJECT' + ) + if project_id is None: + app_id = os.environ.get('GAE_APPLICATION') + project_id = app_id.split('~', 1)[1] + return project_id def get_current_module_name(): """Returns the module name of the current instance. @@ -124,43 +125,34 @@ def get_current_instance_id(): return os.environ.get('GAE_INSTANCE') or os.environ.get('INSTANCE_ID', None) -def _GetRpc(): - return apiproxy_stub_map.UserRPC('modules') +class _ThreadedRpc: + """A class to emulate the UserRPC object for threaded operations.""" + def __init__(self, target): + self.thread = threading.Thread(target=self._run_target, args=(target,)) + self.exception = None + self.done = threading.Event() + self.thread.start() -def _MakeAsyncCall(method, request, response, get_result_hook): - rpc = _GetRpc() - rpc.make_call(method, request, response, get_result_hook) - return rpc + def _run_target(self, target): + try: + target() + except Exception as e: + self.exception = e + finally: + self.done.set() + def wait(self): + self.done.wait() -_MODULE_SERVICE_ERROR_MAP = { - modules_service_pb2.ModulesServiceError.INVALID_INSTANCES: - InvalidInstancesError, - modules_service_pb2.ModulesServiceError.INVALID_MODULE: - InvalidModuleError, - modules_service_pb2.ModulesServiceError.INVALID_VERSION: - InvalidVersionError, - modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR: - TransientError, - modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE: - UnexpectedStateError -} + def check_success(self): + if self.exception: + raise self.exception - -def _CheckAsyncResult(rpc, expected_application_errors, - ignored_application_errors): - try: - rpc.check_success() - except apiproxy_errors.ApplicationError as e: - if e.application_error in ignored_application_errors: - logging.info(ignored_application_errors.get(e.application_error)) - return - if e.application_error in expected_application_errors: - mapped_error = _MODULE_SERVICE_ERROR_MAP.get(e.application_error) - if mapped_error: - raise mapped_error() - raise Error(e) + def get_result(self): + self.wait() + self.check_success() + return None def get_modules(): @@ -171,19 +163,25 @@ def get_modules(): application. The 'default' module will be included if it exists, as will the name of the module that is associated with the instance that calls this function. + + Raises: + Error: If the configured project ID is invalid. + TransientError: If there is an issue fetching the information. """ - project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') - if project is None: - appId = os.environ.get('GAE_APPLICATION') - project = appId.split('~', 1)[1] - parent = 'apps/' + project - client = discovery.build('appengine', 'v1') - request = client.apps().services().list(appsId=project) - request.headers['X-Goog-Api-Client'] = 'appengine-modules-api-python-client' - response = request.execute() - + project_id = _get_project_id() + http_client = httplib2.Http() + http_client = http.set_user_agent(http_client, "appengine-modules-api-python-client") + authorized_http = credentials.authorize(http_client) + client = discovery.build('appengine', 'v1', http=authorized_http) + request = client.apps().services().list(appsId=project_id) + try: + response = request.execute() + except errors.HttpError as e: + if e.resp.status == 404: + raise Error(f"Project '{project_id}' not found.") from e + _raise_error(e) + return [service['id'] for service in response.get('services', [])] - def get_versions(module=None): @@ -201,17 +199,22 @@ def get_versions(module=None): `InvalidModuleError` if the given module isn't valid, `TransientError` if there is an issue fetching the information. """ + if not module: module = os.environ.get('GAE_SERVICE', 'default') - project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') - if project is None: - appId = os.environ.get('GAE_APPLICATION') - project = appId.split('~', 1)[1] + + project_id = _get_project_id() client = discovery.build('appengine', 'v1') request = client.apps().services().versions().list( - appsId=project, servicesId=module, view='FULL') - response = request.execute() - + appsId=project_id, servicesId=module, view='FULL' + ) + try: + response = request.execute() + except errors.HttpError as e: + if e.resp.status == 404: + raise InvalidModuleError(f"Module '{module}' not found.") from e + _raise_error(e) + return [version['id'] for version in response.get('versions', [])] @@ -229,36 +232,42 @@ def get_default_version(module=None): `InvalidModuleError` if the given module is not valid, `InvalidVersionError` if no default version could be found. """ + if not module: module = os.environ.get('GAE_SERVICE', 'default') - project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') - if project is None: - appId = os.environ.get('GAE_APPLICATION') - project = appId.split('~', 1)[1] + project = _get_project_id() client = discovery.build('appengine', 'v1') request = client.apps().services().get( appsId=project, servicesId=module) - - response = request.execute() - + + try: + response = request.execute() + except errors.HttpError as e: + if e.resp.status == 404: + raise InvalidModuleError(f"Module '{module}' not found.") from e + _raise_error(e) + allocations = response.get('split', {}).get('allocations') maxAlloc = -1 retVersion = None - for version, allocation in allocations.items() : - if allocation == 1.0: - retVersion = version - break - - if allocation > maxAlloc : - retVersion = version - maxAlloc = allocation - elif allocation == maxAlloc: - if version < retVersion: + + if allocations: + for version, allocation in allocations.items(): + if allocation == 1.0: retVersion = version - + break + + if allocation > maxAlloc: + retVersion = version + maxAlloc = allocation + elif allocation == maxAlloc: + if version < retVersion: + retVersion = version + + if retVersion is None: + raise InvalidVersionError(f"Could not determine default version for module '{module}'.") + return retVersion - - def get_num_instances( @@ -287,25 +296,34 @@ def get_num_instances( if module is None: module = get_current_module_name() - + if version is None: version = get_current_version_name() - - project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') - if project is None: - appId = os.environ.get('GAE_APPLICATION') - project = appId.split('~', 1)[1] - + + project_id = _get_project_id() + client = discovery.build('appengine', 'v1') request = client.apps().services().versions().get( - appsId=project, servicesId=module, versionsId=version) - - response = request.execute() + appsId=project_id, servicesId=module, versionsId=version) + + try: + response = request.execute() + except errors.HttpError as e: + _raise_error(e) + if 'manualScaling' in response: return response['manualScaling'].get('instances') - + return 0 +def _admin_api_version_patch(project_id, module, version, body, update_mask): + client = discovery.build('appengine', 'v1') + client.apps().services().versions().patch( + appsId=project_id, + servicesId=module, + versionsId=version, + updateMask=update_mask, + body=body).execute() def set_num_instances( instances, @@ -333,80 +351,41 @@ def set_num_instances_async( instances, module=None, version=None): - """Returns a `UserRPC` to set the number of instances on the module version. - - Args: - instances: The number of instances to set. - module: The module to set the number of instances for, if `None` the current - module will be used. - version: The version set the number of instances for, if `None` the current - version will be used. - - Returns: - A `UserRPC` to set the number of instances on the module version. - """ - - class _ThreadedRpc: - """A class to emulate the UserRPC object for threaded operations.""" - - def __init__(self, target): - self.thread = threading.Thread(target=self._run_target, args=(target,)) - self.exception = None - self.done = threading.Event() - self.thread.start() - - def _run_target(self, target): - try: - target() - except Exception as e: - self.exception = e - finally: - self.done.set() - - def get_result(self): - self.done.wait() - if self.exception: - # Re-raise the exception caught in the thread - raise self.exception - - if not isinstance(instances, six.integer_types): - raise TypeError("'instances' arg must be of type long or int.") - - project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] - if module is None: - module = get_current_module_name() - if version is None: - version = get_current_version_name() - - def run_request(): - """This function will be executed in a separate thread.""" - try: - client = discovery.build('appengine', 'v1') - body = { - 'manualScaling': { - 'instances': instances - } - } - update_mask = 'manualScaling.instances' - client.apps().services().versions().patch( - appsId=project_id, - servicesId=module, - versionsId=version, - updateMask=update_mask, - body=body).execute() - except discovery.HttpError as e: - # Translate HTTP errors to the exceptions expected by the API - if e.resp.status == 400: - raise InvalidInstancesError(e) from e - elif e.resp.status == 404: - raise InvalidVersionError(e) from e - elif e.resp.status >= 500: - raise TransientError(e) from e - else: - raise Error(e) from e - - return _ThreadedRpc(target=run_request) + """Returns a `UserRPC` to set the number of instances on the module version. + + Args: + instances: The number of instances to set. + module: The module to set the number of instances for, if `None` the current + module will be used. + version: The version set the number of instances for, if `None` the current + version will be used. + + Returns: + A `UserRPC` to set the number of instances on the module version. + """ + + if not isinstance(instances, six.integer_types): + raise TypeError("'instances' arg must be of type long or int.") + project_id = _get_project_id() + if module is None: + module = get_current_module_name() + if version is None: + version = get_current_version_name() + + def run_request(): + """This function will be executed in a separate thread.""" + try: + body = { + 'manualScaling': { + 'instances': instances + } + } + _admin_api_version_patch(project_id, module, version, body, 'manualScaling.instances') + except errors.HttpError as e: + _raise_error(e) + + return _ThreadedRpc(target=run_request) def start_version(module, version): """Start all instances for the given version of the module. @@ -423,69 +402,35 @@ def start_version(module, version): rpc.get_result() -def start_version_async(module, version): - """Returns a `UserRPC` to start the module version. - - Args: - module: The module to start. - version: The version to start. - - Returns: - A `UserRPC` to start the module version. - """ - - class _ThreadedRpc: - """A class to emulate the UserRPC object for threaded operations.""" - - def __init__(self, target): - self.thread = threading.Thread(target=self._run_target, args=(target,)) - self.exception = None - self.done = threading.Event() - self.thread.start() - - def _run_target(self, target): - try: - target() - except Exception as e: - self.exception = e - finally: - self.done.set() - - def wait(self): - self.done.wait() - - def check_success(self): - if self.exception: - raise self.exception - - def get_result(self): - self.wait() - self.check_success() - return None - - project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] - - def run_request(): - """This function will be executed in a separate thread.""" - try: - client = discovery.build('appengine', 'v1') - body = {'servingStatus': 'SERVING'} - update_mask = 'servingStatus' - client.apps().services().versions().patch( - appsId=project_id, - servicesId=module, - versionsId=version, - updateMask=update_mask, - body=body).execute() - except discovery.HttpError as e: - if e.resp.status == 404: - raise InvalidVersionError(e) from e - elif e.resp.status >= 500: - raise TransientError(e) from e - else: - raise Error(e) from e - - return _ThreadedRpc(target=run_request) +def start_version_async( + module, + version): + """Returns a `UserRPC` to start all instances for the given module version. + + Args: + module: String containing the name of the module to affect. + version: String containing the name of the version of the module to start. + + Returns: + A `UserRPC` to start all instances for the given module version. + """ + if module is None: + module = get_current_module_name() + + if version is None: + version = get_current_version_name() + project_id = _get_project_id() + def run_request(): + """This function will be executed in a separate thread.""" + try: + body = { + 'servingStatus': 'SERVING' + } + _admin_api_version_patch(project_id, module, version, body, 'servingStatus') + except errors.HttpError as e: + _raise_error(e) + + return _ThreadedRpc(target=run_request) def stop_version( @@ -506,69 +451,47 @@ def stop_version( rpc.get_result() -def stop_version_async(module, version): - """Returns a `UserRPC` to stop the module version. - - Args: - module: The module to stop. - version: The version to stop. - - Returns: - A `UserRPC` to stop the module version. - """ - class _ThreadedRpc: - """A class to emulate the UserRPC object for threaded operations.""" - - def __init__(self, target): - self.thread = threading.Thread(target=self._run_target, args=(target,)) - self.exception = None - self.done = threading.Event() - self.thread.start() - - def _run_target(self, target): - try: - target() - except Exception as e: - self.exception = e - finally: - self.done.set() - - def wait(self): - self.done.wait() - - def check_success(self): - if self.exception: - raise self.exception - - def get_result(self): - self.wait() - self.check_success() - return None - - project_id = os.environ.get('GAE_APPLICATION', '').split('~')[-1] - - def run_request(): - """This function will be executed in a separate thread.""" - try: - client = discovery.build('appengine', 'v1') - body = {'servingStatus': 'STOPPED'} - update_mask = 'servingStatus' - client.apps().services().versions().patch( - appsId=project_id, - servicesId=module, - versionsId=version, - updateMask=update_mask, - body=body).execute() - except discovery.HttpError as e: - if e.resp.status == 404: - raise InvalidVersionError(e) from e - elif e.resp.status >= 500: - raise TransientError(e) from e - else: - raise Error(e) from e - - return _ThreadedRpc(target=run_request) +def stop_version_async( + module=None, + version=None): + """Returns a `UserRPC` to stop all instances for the given module version. + + Args: + module: The module to affect, if `None` the current module is used. + version: The version of the given module to affect, if `None` the current + version is used. + Returns: + A `UserRPC` to stop all instances for the given module version. + """ + if module is None: + module = get_current_module_name() + + if version is None: + version = get_current_version_name() + project_id = _get_project_id() + def run_request(): + """This function will be executed in a separate thread.""" + try: + body = { + 'servingStatus': 'STOPPED' + } + _admin_api_version_patch(project_id, module, version, body, 'servingStatus') + except errors.HttpError as e: + _raise_error(e) + + return _ThreadedRpc(target=run_request) + +def _construct_hostname(instance, version, module, default_hostname): + """Constructs a hostname for the given module, version, and instance.""" + hostname_parts = [] + if instance: + hostname_parts.append(instance) + hostname_parts.append(version) + hostname_parts.append(module) + hostname_parts.append(default_hostname) + + return ".".join(hostname_parts) def get_hostname( module=None, @@ -597,87 +520,15 @@ def get_hostname( if module is None: module = get_current_module_name() - + if version is None: version = get_current_version_name() - - project = os.environ.get('GAE_PROJECT') or os.environ.get('GOOGLE_CLOUD_PROJECT') - if project is None: - appId = os.environ.get('GAE_APPLICATION') - project = appId.split('~', 1)[1] - + + project = _get_project_id() + client = discovery.build('appengine', 'v1') request = client.apps().get(appsId=project) response = request.execute() default_hostname = response.get('defaultHostname') - - hostname_parts = [] - if instance: - hostname_parts.append(instance) - hostname_parts.append(version) - hostname_parts.append(module) - hostname_parts.append(default_hostname) - - return ".".join(hostname_parts) - - -def get_current_region(): - """ - Dynamically determines the current GCP region by querying the metadata service. - This is the most reliable way to get the region from within a GCP environment. - - Returns: - str: The GCP region (e.g., 'us-central1'), or None if not found. - """ - try: - # google-auth can automatically fetch the project ID and other metadata - # in a GCP environment. We can get the region from the instance metadata. - scoped_credentials, _ = google.auth.default( - scopes=['https://www.googleapis.com/auth/cloud-platform']) - - # The request object is needed to make authenticated calls - authed_session = requests.AuthorizedSession(scoped_credentials) - - # The metadata server URL for the instance's zone - metadata_url = 'http://metadata.google.internal/computeMetadata/v1/instance/zone' - metadata_response = authed_session.get( - metadata_url, - headers={'Metadata-Flavor': 'Google'} - ) - metadata_response.raise_for_status() # Raises an HTTPError for bad responses - - # The response is in the format 'projects/PROJECT_NUM/zones/ZONE'. - # We extract the zone (e.g., 'us-central1-a'). - zone = metadata_response.text.split('/')[-1] - - # The region is the zone without the final letter. - return zone[:-2] - - except Exception: - return None - -def create_regional_admin_client(): - """ - Creates an App Engine Admin API client configured for the region - where the code is currently running. - - Returns: - A Google API client resource object for the current region, or a global - client if the region cannot be determined. - """ - region = get_current_region() - - if not region: - print("Warning: Could not determine region. Falling back to global endpoint.") - return AppEngineAdminClient() - - print(f"Detected region: {region}. Creating regional client.") - - # The regional endpoint format for the App Engine Admin API - regional_endpoint = f'https://{region}-appengine.googleapis.com' - - client_opts = ClientOptions(api_endpoint=regional_endpoint) - # Build the service object, passing the regional endpoint URL - admin_api_client = AppEngineAdminClient(client_options=client_options) - return admin_api_client + return _construct_hostname(instance, version, module, default_hostname) diff --git a/tox.ini b/tox.ini index 3e65358..831ccf0 100644 --- a/tox.ini +++ b/tox.ini @@ -30,4 +30,5 @@ deps = ruamel.yaml < 0.18 six urllib3 + google-api-python-client commands = pytest --cov=google.appengine {posargs} From 1a652966dafbb8c1dba1e10255ee21fb07b8136e Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 13 Nov 2025 05:26:10 +0000 Subject: [PATCH 43/57] experimenting with http headers --- src/google/appengine/api/modules/modules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 58294f9..cdb3b4d 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -14,6 +14,7 @@ from google.appengine.api.modules import modules_service_pb2 from google.appengine.runtime import apiproxy_errors from googleapiclient import discovery, errors, http +import google.auth import six import httplib2 @@ -171,6 +172,7 @@ def get_modules(): project_id = _get_project_id() http_client = httplib2.Http() http_client = http.set_user_agent(http_client, "appengine-modules-api-python-client") + credentials = google.auth.default() authorized_http = credentials.authorize(http_client) client = discovery.build('appengine', 'v1', http=authorized_http) request = client.apps().services().list(appsId=project_id) From 1980c02cbdd333fb621a6dd2835fc943b228af57 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 13 Nov 2025 05:31:42 +0000 Subject: [PATCH 44/57] experimenting with http headers --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index cdb3b4d..afcce18 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -172,7 +172,7 @@ def get_modules(): project_id = _get_project_id() http_client = httplib2.Http() http_client = http.set_user_agent(http_client, "appengine-modules-api-python-client") - credentials = google.auth.default() + credentials,_ = google.auth.default() authorized_http = credentials.authorize(http_client) client = discovery.build('appengine', 'v1', http=authorized_http) request = client.apps().services().list(appsId=project_id) From 2cc7534a213ab04cd06466ef9c10bd34dca89bd8 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 13 Nov 2025 05:43:47 +0000 Subject: [PATCH 45/57] experimenting with http headers --- src/google/appengine/api/modules/modules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index afcce18..578ceff 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -14,6 +14,7 @@ from google.appengine.api.modules import modules_service_pb2 from google.appengine.runtime import apiproxy_errors from googleapiclient import discovery, errors, http +from google_auth_httplib2 import AuthorizedHttp import google.auth import six import httplib2 @@ -173,7 +174,7 @@ def get_modules(): http_client = httplib2.Http() http_client = http.set_user_agent(http_client, "appengine-modules-api-python-client") credentials,_ = google.auth.default() - authorized_http = credentials.authorize(http_client) + authorized_http = AuthorizedHttp(credentials, http=http_client) client = discovery.build('appengine', 'v1', http=authorized_http) request = client.apps().services().list(appsId=project_id) try: From 60c88d011c5b8dcbf1ad77aa2732030d74d1da71 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 13 Nov 2025 06:05:01 +0000 Subject: [PATCH 46/57] experimenting with http headers --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 578ceff..b66895c 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -171,7 +171,7 @@ def get_modules(): TransientError: If there is an issue fetching the information. """ project_id = _get_project_id() - http_client = httplib2.Http() + http_client = httplib2.Http(timeout=60) http_client = http.set_user_agent(http_client, "appengine-modules-api-python-client") credentials,_ = google.auth.default() authorized_http = AuthorizedHttp(credentials, http=http_client) From 10cb4a71447a770d60c1c8e2ff3ea00175c79c6e Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 13 Nov 2025 06:19:01 +0000 Subject: [PATCH 47/57] experimenting with http headers --- src/google/appengine/api/modules/modules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index b66895c..bf76ab4 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -19,6 +19,8 @@ import six import httplib2 +httplib2.debuglevel = 1 + __all__ = [ 'Error', From 14404805670aaeac7349c0b9c70f0ea8f9e199f3 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 19 Nov 2025 09:06:04 +0000 Subject: [PATCH 48/57] refactoring getHostname --- src/google/appengine/api/modules/modules.py | 112 +++++++++++++++----- 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index bf76ab4..829e055 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -13,11 +13,8 @@ from google.appengine.api import apiproxy_stub_map from google.appengine.api.modules import modules_service_pb2 from google.appengine.runtime import apiproxy_errors -from googleapiclient import discovery, errors, http -from google_auth_httplib2 import AuthorizedHttp -import google.auth +from googleapiclient import discovery, errors import six -import httplib2 httplib2.debuglevel = 1 @@ -173,11 +170,7 @@ def get_modules(): TransientError: If there is an issue fetching the information. """ project_id = _get_project_id() - http_client = httplib2.Http(timeout=60) - http_client = http.set_user_agent(http_client, "appengine-modules-api-python-client") - credentials,_ = google.auth.default() - authorized_http = AuthorizedHttp(credentials, http=http_client) - client = discovery.build('appengine', 'v1', http=authorized_http) + client = discovery.build('appengine', 'v1') request = client.apps().services().list(appsId=project_id) try: response = request.execute() @@ -487,15 +480,8 @@ def run_request(): return _ThreadedRpc(target=run_request) -def _construct_hostname(instance, version, module, default_hostname): +def _construct_hostname(*hostname_parts): """Constructs a hostname for the given module, version, and instance.""" - hostname_parts = [] - if instance: - hostname_parts.append(instance) - hostname_parts.append(version) - hostname_parts.append(module) - hostname_parts.append(default_hostname) - return ".".join(hostname_parts) def get_hostname( @@ -523,17 +509,91 @@ def get_hostname( TypeError: if the given instance type is invalid. """ - if module is None: - module = get_current_module_name() + if client is None: + client = discovery.build('appengine', 'v1') + + project_id = _get_project_id() + + req_module = module or get_current_module_name() + # If version is not specified, we will use the version of the current context. + req_version = version or get_current_version_name() + + try: + # Get the application's services to check for the legacy "no-engine" case. + services = self.get_modules() + + # Get the application's default hostname + request = client.apps().get(appsId=project_id) + response = request.execute() + default_hostname = response.get('defaultHostname') + + except errors.HttpError as e: + _raise_error(e) + + # Legacy Applications (Without "Engines") + if len(services) == 1 and services[0]['id'] == 'default': + if req_module != 'default': + raise InvalidModuleError(f"Module '{req_module}' not found.") + hostname_parts = [req_version, default_hostname] + if instance: + return _construct_hostname(instance, req_version, default_hostname) + return _construct_hostname(req_version, default_hostname) + + # --- Cases for modern applications with one or more services --- + if instance is not None: + # Request for a specific instance + try: + instance_id = int(instance) + if instance_id < 0: + raise ValueError + except (ValueError, TypeError) as e: + raise InvalidInstancesError("Instance must be a non-negative integer.") from e + + try: + # Get version details to check scaling and instance count + request = client.apps().services().versions().get( + appsId=project_id, servicesId=req_module, versionsId=req_version, view='FULL') + version_details = request.execute() + + if 'manualScaling' not in version_details: + raise InvalidInstancesError( + "Instance-specific hostnames are only available for manually scaled services.") + + num_instances = version_details['manualScaling'].get('instances', 0) + if instance_id >= num_instances: + raise InvalidInstancesError( + "The specified instance does not exist for this module/version.") + + return _construct_hostname(instance, req_version, req_module, default_hostname) + + except errors.HttpError as e: + if e.resp.status == 404: + raise InvalidModuleError( + f"Module '{req_module}' or version '{req_version}' not found.") from e + _raise_error(e) + + # Request with no explicit version and no instance. if version is None: - version = get_current_version_name() + try: + # Get all versions for the target module. + versions_list = self.get_versions(module=req_module) - project = _get_project_id() + # Create a set of version IDs for efficient lookup. + existing_version_ids = {v['id'] for v in versions_list} - client = discovery.build('appengine', 'v1') - request = client.apps().get(appsId=project) - response = request.execute() - default_hostname = response.get('defaultHostname') + # Check if the version from the current context exists in the target module. + if req_version in existing_version_ids: + return _construct_hostname(req_version, req_module, default_hostname) + else: + # If the current version does not exist on the target module, + # return a hostname without a version. + return _construct_hostname(req_module, default_hostname) + + except errors.HttpError as e: + if e.resp.status == 404: + raise InvalidModuleError(f"Module '{req_module}' not found.") from e + _raise_error(e) - return _construct_hostname(instance, version, module, default_hostname) + # Request with a version but no instance + return _construct_hostname(version, req_module, default_hostname) From 5cbb9b4ce10774a5f4cb4f931087d2af5261cfd3 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 19 Nov 2025 09:12:29 +0000 Subject: [PATCH 49/57] refactoring getHostname --- src/google/appengine/api/modules/modules.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 829e055..4e9bfb8 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -16,9 +16,6 @@ from googleapiclient import discovery, errors import six -httplib2.debuglevel = 1 - - __all__ = [ 'Error', 'InvalidModuleError', From 0e25409e849cfe07f866f94505d386ebec1ada5f Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 19 Nov 2025 09:15:47 +0000 Subject: [PATCH 50/57] refactoring getHostname --- src/google/appengine/api/modules/modules.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 4e9bfb8..81120f4 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -506,9 +506,6 @@ def get_hostname( TypeError: if the given instance type is invalid. """ - if client is None: - client = discovery.build('appengine', 'v1') - project_id = _get_project_id() req_module = module or get_current_module_name() From e7d8b0edcf22634cbb74ee0d187215f353c29a26 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 19 Nov 2025 09:18:09 +0000 Subject: [PATCH 51/57] refactoring getHostname --- src/google/appengine/api/modules/modules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index 81120f4..e5e6654 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -507,7 +507,8 @@ def get_hostname( """ project_id = _get_project_id() - + client = discovery.build('appengine', 'v1') + req_module = module or get_current_module_name() # If version is not specified, we will use the version of the current context. req_version = version or get_current_version_name() From 7d1603ff672aa1808dc41c176fab508da6096a9b Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 19 Nov 2025 09:21:52 +0000 Subject: [PATCH 52/57] refactoring getHostname --- src/google/appengine/api/modules/modules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index e5e6654..c15a84d 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -515,7 +515,7 @@ def get_hostname( try: # Get the application's services to check for the legacy "no-engine" case. - services = self.get_modules() + services = get_modules() # Get the application's default hostname request = client.apps().get(appsId=project_id) @@ -572,7 +572,7 @@ def get_hostname( if version is None: try: # Get all versions for the target module. - versions_list = self.get_versions(module=req_module) + versions_list = get_versions(module=req_module) # Create a set of version IDs for efficient lookup. existing_version_ids = {v['id'] for v in versions_list} From 05ed31fcbf8c95a4761a99338c7172d3db1cf638 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 19 Nov 2025 09:28:26 +0000 Subject: [PATCH 53/57] refactoring getHostname --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index c15a84d..e652e4a 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -575,7 +575,7 @@ def get_hostname( versions_list = get_versions(module=req_module) # Create a set of version IDs for efficient lookup. - existing_version_ids = {v['id'] for v in versions_list} + existing_version_ids = set(versions_list) # Check if the version from the current context exists in the target module. if req_version in existing_version_ids: From 3e8b3c0e0e1734f181c13e0ae5237276b4a5da3b Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Thu, 20 Nov 2025 09:03:57 +0000 Subject: [PATCH 54/57] Adding tests for getHostname --- .coverage | Bin 118784 -> 118784 bytes src/google/appengine/api/modules/modules.py | 27 ++- .../appengine/api/modules/modules_test.py | 196 +++++++++++++++++- 3 files changed, 205 insertions(+), 18 deletions(-) diff --git a/.coverage b/.coverage index 76b688eb0057e7411fd828b40870c05310e22520..0e17ecb45059c889baf0e4f15b4a1c4cfb2cf8f8 100644 GIT binary patch delta 143 zcmV;A0C4|+pa+1U2e1Z!9$)|u`48+5$PZo*M-M#?=?=#ZxelZbmkxOjZ4O|w5fD5M zlWC5LR8j{80SSR51p@91005Yonf2>Zkl-c&$iA=L69E260KncS0f67aOU=G0EUG)F}fH$nl!4`G*3M?4`0fRk{w>^&m!$7B>Hn0Ey delta 134 zcmV;10D1p_pa+1U2e1Z!9%%p%`48+5$PZo*M-M#?@($1r!Vawtpbml#b`EK?5fDWV zlVFaCOGyU>0SSQ;1p@99001yEGwau-Ai+%lkbPgfCjk7D0D!$u0sz0y9lzxO0ATCh o`qu!!GyvGwa|K`mVDBymz#M?RyZZG20AT9{gFTP8J&ysyKw&yE)&Kwi diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index e652e4a..ecdf413 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -505,10 +505,23 @@ def get_hostname( InvalidInstancesError: if the given instance value is invalid. TypeError: if the given instance type is invalid. """ + # + # START FIX: Validate instance parameter at the beginning of the function. + # + if instance is not None: + try: + instance_id = int(instance) + if instance_id < 0: + raise ValueError + except (ValueError, TypeError) as e: + raise InvalidInstancesError("Instance must be a non-negative integer.") from e + # + # END FIX + # project_id = _get_project_id() client = discovery.build('appengine', 'v1') - + req_module = module or get_current_module_name() # If version is not specified, we will use the version of the current context. req_version = version or get_current_version_name() @@ -526,7 +539,7 @@ def get_hostname( _raise_error(e) # Legacy Applications (Without "Engines") - if len(services) == 1 and services[0]['id'] == 'default': + if len(services) == 1 and services[0] == 'default': if req_module != 'default': raise InvalidModuleError(f"Module '{req_module}' not found.") hostname_parts = [req_version, default_hostname] @@ -538,13 +551,6 @@ def get_hostname( if instance is not None: # Request for a specific instance - try: - instance_id = int(instance) - if instance_id < 0: - raise ValueError - except (ValueError, TypeError) as e: - raise InvalidInstancesError("Instance must be a non-negative integer.") from e - try: # Get version details to check scaling and instance count request = client.apps().services().versions().get( @@ -556,7 +562,7 @@ def get_hostname( "Instance-specific hostnames are only available for manually scaled services.") num_instances = version_details['manualScaling'].get('instances', 0) - if instance_id >= num_instances: + if int(instance) >= num_instances: raise InvalidInstancesError( "The specified instance does not exist for this module/version.") @@ -592,3 +598,4 @@ def get_hostname( # Request with a version but no instance return _construct_hostname(version, req_module, default_hostname) + diff --git a/tests/google/appengine/api/modules/modules_test.py b/tests/google/appengine/api/modules/modules_test.py index 34e8ddb..b41572f 100755 --- a/tests/google/appengine/api/modules/modules_test.py +++ b/tests/google/appengine/api/modules/modules_test.py @@ -465,21 +465,201 @@ def testRaiseError_Generic(self): with self.assertRaises(modules.Error): modules.stop_version() - # --- Tests for get_hostname (intentionally left incomplete) --- + # --- Tests for get_hostname --- - def testGetHostname(self): - # This test verifies the current non-error-handled behavior of get_hostname + def testGetHostname_WithVersion_NoInstance(self): + """Tests the simple case with an explicit module and version.""" self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + mock_apps = self.mox.CreateMockAnything() - mock_request = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + self.mock_admin_api_client.apps().AndReturn(mock_apps) - mock_apps.get(appsId='project').AndReturn(mock_request) - mock_request.execute().AndReturn( + modules.get_modules().AndReturn(['default', 'other']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) + + self.mox.ReplayAll() + self.assertEqual('v2.foo.project.appspot.com', + modules.get_hostname(module='foo', version='v2')) + + def testGetHostname_Instance_Success(self): + """Tests a successful request for a specific instance.""" + self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + mock_version_request = self.mox.CreateMockAnything() + + # Expect first call to .apps() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + modules.get_modules().AndReturn(['default', 'other']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + + # Expect second call to .apps() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.get( + appsId='project', servicesId='default', versionsId='v1', + view='FULL').AndReturn(mock_version_request) + mock_version_request.execute().AndReturn( + {'manualScaling': {'instances': 5}}) + self.mox.ReplayAll() - self.assertEqual('i.v1.default.project.appspot.com', - modules.get_hostname(instance='i')) + self.assertEqual('2.v1.default.project.appspot.com', + modules.get_hostname(instance='2')) + + def testGetHostname_Instance_NoManualScaling(self): + """Tests instance request for a service without manual scaling.""" + self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + mock_version_request = self.mox.CreateMockAnything() + # Expect first call to .apps() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + modules.get_modules().AndReturn(['default', 'other']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + + # Expect second call to .apps() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.get( + appsId='project', servicesId='default', versionsId='v1', + view='FULL').AndReturn(mock_version_request) + mock_version_request.execute().AndReturn({'automaticScaling': {}}) + + self.mox.ReplayAll() + with self.assertRaisesRegex( + modules.InvalidInstancesError, + 'Instance-specific hostnames are only available for manually scaled ' + 'services.'): + modules.get_hostname(instance='1') + + def testGetHostname_Instance_OutOfBounds(self): + """Tests instance request where the instance ID is out of bounds.""" + self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + + mock_apps = self.mox.CreateMockAnything() + mock_services = self.mox.CreateMockAnything() + mock_versions = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + mock_version_request = self.mox.CreateMockAnything() + + # Expect first call to .apps() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + modules.get_modules().AndReturn(['default', 'other']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + + # Expect second call to .apps() + self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_apps.services().AndReturn(mock_services) + mock_services.versions().AndReturn(mock_versions) + mock_versions.get( + appsId='project', servicesId='default', versionsId='v1', + view='FULL').AndReturn(mock_version_request) + mock_version_request.execute().AndReturn( + {'manualScaling': {'instances': 5}}) + + self.mox.ReplayAll() + with self.assertRaisesRegex( + modules.InvalidInstancesError, + 'The specified instance does not exist for this module/version.'): + modules.get_hostname(instance='5') + + def testGetHostname_Instance_InvalidValue(self): + """Tests instance request with an invalid non-integer instance value.""" + # This test is now simpler. Because the validation happens at the very + # top of get_hostname, no API calls are made, so no mocks are needed. + with self.assertRaisesRegex( + modules.InvalidInstancesError, + 'Instance must be a non-negative integer.'): + modules.get_hostname(instance='foo') + + def testGetHostname_NoVersion_VersionExistsOnTarget(self): + """Tests no-version call where the current version exists on the target.""" + self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + self.mox.StubOutWithMock(modules, 'get_versions') + + mock_apps = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + + self.mock_admin_api_client.apps().AndReturn(mock_apps) + modules.get_modules().AndReturn(['default', 'module1']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + modules.get_versions(module='module1').AndReturn(['v1', 'v2']) + + self.mox.ReplayAll() + self.assertEqual('v1.module1.project.appspot.com', + modules.get_hostname(module='module1')) + + def testGetHostname_NoVersion_VersionDoesNotExistOnTarget(self): + """Tests no-version call where the current version is not on the target.""" + self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + self.mox.StubOutWithMock(modules, 'get_versions') + + mock_apps = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + + self.mock_admin_api_client.apps().AndReturn(mock_apps) + modules.get_modules().AndReturn(['default', 'module1']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + modules.get_versions(module='module1').AndReturn(['v2', 'v3']) + + self.mox.ReplayAll() + self.assertEqual('module1.project.appspot.com', + modules.get_hostname(module='module1')) + + def testGetHostname_LegacyApp_Success(self): + """Tests a hostname request for a legacy app without engines.""" + self._SetupAdminApiMocks() + self.mox.StubOutWithMock(modules, 'get_modules') + + mock_apps = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + + self.mock_admin_api_client.apps().AndReturn(mock_apps) + modules.get_modules().AndReturn(['default']) + mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_get_request.execute().AndReturn( + {'defaultHostname': 'project.appspot.com'}) + + self.mox.ReplayAll() + self.assertEqual('v1.project.appspot.com', modules.get_hostname()) + + def testGetHostname_LegacyApp_WithInstance(self): + """Tests a legacy app request with an invalid non-integer instance.""" + # This test was incorrect. It should expect an error for instance='i'. + # Like the test above, no mocks are needed because it fails on validation + # before any API calls are made. + with self.assertRaisesRegex( + modules.InvalidInstancesError, + 'Instance must be a non-negative integer.'): + modules.get_hostname(instance='i') if __name__ == '__main__': absltest.main() From d6945203b463acd40519e129db74e86e3503b778 Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 10 Dec 2025 08:29:33 +0000 Subject: [PATCH 55/57] commiting final changes --- src/google/appengine/api/modules/modules.py | 266 ++++- .../appengine/api/modules/modules_test.py | 981 +++++++++++++++--- 2 files changed, 1098 insertions(+), 149 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index ecdf413..af49129 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -13,8 +13,12 @@ from google.appengine.api import apiproxy_stub_map from google.appengine.api.modules import modules_service_pb2 from google.appengine.runtime import apiproxy_errors -from googleapiclient import discovery, errors +from googleapiclient import discovery, errors, http +from google_auth_httplib2 import AuthorizedHttp +import google.auth import six +import httplib2 + __all__ = [ 'Error', @@ -63,6 +67,9 @@ class UnexpectedStateError(Error): class TransientError(Error): """A transient error was encountered, retry the operation.""" +def _has_opted_in(): + return (os.environ.get('OPT_IN_MODULES', 'false').lower() == 'true') + def _raise_error(e): # Translate HTTP errors to the exceptions expected by the API if e.resp.status == 400: @@ -153,6 +160,50 @@ def get_result(self): return None +def _GetRpc(): + return apiproxy_stub_map.UserRPC('modules') + +def _MakeAsyncCall(method, request, response, get_result_hook): + rpc = _GetRpc() + rpc.make_call(method, request, response, get_result_hook) + return rpc + +_MODULE_SERVICE_ERROR_MAP = { + modules_service_pb2.ModulesServiceError.INVALID_INSTANCES: + InvalidInstancesError, + modules_service_pb2.ModulesServiceError.INVALID_MODULE: + InvalidModuleError, + modules_service_pb2.ModulesServiceError.INVALID_VERSION: + InvalidVersionError, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR: + TransientError, + modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE: + UnexpectedStateError +} + +def _CheckAsyncResult(rpc, expected_application_errors, + ignored_application_errors): + try: + rpc.check_success() + except apiproxy_errors.ApplicationError as e: + if e.application_error in ignored_application_errors: + logging.info(ignored_application_errors.get(e.application_error)) + return + if e.application_error in expected_application_errors: + mapped_error = _MODULE_SERVICE_ERROR_MAP.get(e.application_error) + if mapped_error: + raise mapped_error() + raise Error(e) + +def _get_admin_api_client_with_useragent(methodName): + userAgent = 'appengine-modules-api-python-client/' + methodName + http_client = httplib2.Http(timeout=60) + http_client = http.set_user_agent(http_client, userAgent) + credentials,_ = google.auth.default() + authorized_http = AuthorizedHttp(credentials, http=http_client) + client = discovery.build('appengine', 'v1', http=authorized_http) + return client + def get_modules(): """Returns a list of all modules for the application. @@ -166,9 +217,13 @@ def get_modules(): Error: If the configured project ID is invalid. TransientError: If there is an issue fetching the information. """ + if not _has_opted_in(): + return get_modules_legacy() + project_id = _get_project_id() - client = discovery.build('appengine', 'v1') + client = _get_admin_api_client_with_useragent('get_modules') request = client.apps().services().list(appsId=project_id) + try: response = request.execute() except errors.HttpError as e: @@ -178,6 +233,20 @@ def get_modules(): return [service['id'] for service in response.get('services', [])] +#Legacy get_modules implementation +def get_modules_legacy(): + def _ResultHook(rpc): + _CheckAsyncResult(rpc, [], {}) + + + return rpc.response.module + + request = modules_service_pb2.GetModulesRequest() + response = modules_service_pb2.GetModulesResponse() + return _MakeAsyncCall('GetModules', request, response, + _ResultHook).get_result() + + def get_versions(module=None): """Returns a list of versions for a given module. @@ -194,12 +263,14 @@ def get_versions(module=None): `InvalidModuleError` if the given module isn't valid, `TransientError` if there is an issue fetching the information. """ + if not _has_opted_in(): + return get_versions_legacy(module=module) if not module: module = os.environ.get('GAE_SERVICE', 'default') - + project_id = _get_project_id() - client = discovery.build('appengine', 'v1') + client = _get_admin_api_client_with_useragent('get_versions') request = client.apps().services().versions().list( appsId=project_id, servicesId=module, view='FULL' ) @@ -212,6 +283,25 @@ def get_versions(module=None): return [version['id'] for version in response.get('versions', [])] +def get_versions_legacy(module=None): + def _ResultHook(rpc): + mapped_errors = [ + modules_service_pb2.ModulesServiceError.INVALID_MODULE, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR + ] + _CheckAsyncResult(rpc, mapped_errors, {}) + + + return rpc.response.version + + request = modules_service_pb2.GetVersionsRequest() + if module: + request.module = module + response = modules_service_pb2.GetVersionsResponse() + return _MakeAsyncCall('GetVersions', request, response, + _ResultHook).get_result() + + def get_default_version(module=None): """Returns the name of the default version for the module. @@ -228,10 +318,13 @@ def get_default_version(module=None): if no default version could be found. """ + if not _has_opted_in(): + return get_default_version_legacy(module=module) + if not module: module = os.environ.get('GAE_SERVICE', 'default') project = _get_project_id() - client = discovery.build('appengine', 'v1') + client = _get_admin_api_client_with_useragent('get_default_version') request = client.apps().services().get( appsId=project, servicesId=module) @@ -264,6 +357,22 @@ def get_default_version(module=None): return retVersion +def get_default_version_legacy(module): + def _ResultHook(rpc): + mapped_errors = [ + modules_service_pb2.ModulesServiceError.INVALID_MODULE, + modules_service_pb2.ModulesServiceError.INVALID_VERSION + ] + _CheckAsyncResult(rpc, mapped_errors, {}) + return rpc.response.version + + request = modules_service_pb2.GetDefaultVersionRequest() + if module: + request.module = module + response = modules_service_pb2.GetDefaultVersionResponse() + return _MakeAsyncCall('GetDefaultVersion', request, response, + _ResultHook).get_result() + def get_num_instances( module=None, @@ -288,6 +397,8 @@ def get_num_instances( Raises: `InvalidVersionError` on invalid input. """ + if not _has_opted_in(): + return get_num_instances_legacy(module=module, version=version) if module is None: module = get_current_module_name() @@ -296,8 +407,7 @@ def get_num_instances( version = get_current_version_name() project_id = _get_project_id() - - client = discovery.build('appengine', 'v1') + client = _get_admin_api_client_with_useragent('get_num_instances') request = client.apps().services().versions().get( appsId=project_id, servicesId=module, versionsId=version) @@ -310,9 +420,33 @@ def get_num_instances( return response['manualScaling'].get('instances') return 0 + +def get_num_instances_legacy(module, version): + def _ResultHook(rpc): + mapped_errors = [modules_service_pb2.ModulesServiceError.INVALID_VERSION] + _CheckAsyncResult(rpc, mapped_errors, {}) + return rpc.response.instances + + request = modules_service_pb2.GetNumInstancesRequest() + if module: + request.module = module + if version: + request.version = version + response = modules_service_pb2.GetNumInstancesResponse() + return _MakeAsyncCall('GetNumInstances', request, response, + _ResultHook).get_result() + def _admin_api_version_patch(project_id, module, version, body, update_mask): - client = discovery.build('appengine', 'v1') + methodName = '' + if 'manualScaling' in body: + methodName = 'set_num_instances' + elif 'servingStatus' in body and body['servingStatus'] == 'SERVING': + methodName = 'start_version' + elif 'servingStatus' in body and body['servingStatus'] == 'STOPPED': + methodName = 'stop_version' + + client = _get_admin_api_client_with_useragent(methodName) client.apps().services().versions().patch( appsId=project_id, servicesId=module, @@ -359,6 +493,9 @@ def set_num_instances_async( A `UserRPC` to set the number of instances on the module version. """ + if not _has_opted_in(): + return set_num_instances_async_legacy(instances=instances, module=module, version=version) + if not isinstance(instances, six.integer_types): raise TypeError("'instances' arg must be of type long or int.") @@ -382,6 +519,26 @@ def run_request(): return _ThreadedRpc(target=run_request) +def set_num_instances_async_legacy(instances, module, version): + def _ResultHook(rpc): + mapped_errors = [ + modules_service_pb2.ModulesServiceError.INVALID_VERSION, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR + ] + _CheckAsyncResult(rpc, mapped_errors, {}) + + if not isinstance(instances, six.integer_types): + raise TypeError("'instances' arg must be of type long or int.") + request = modules_service_pb2.SetNumInstancesRequest() + request.instances = instances + if module: + request.module = module + if version: + request.version = version + response = modules_service_pb2.SetNumInstancesResponse() + return _MakeAsyncCall('SetNumInstances', request, response, _ResultHook) + + def start_version(module, version): """Start all instances for the given version of the module. @@ -409,6 +566,9 @@ def start_version_async( Returns: A `UserRPC` to start all instances for the given module version. """ + if not _has_opted_in(): + return start_version_async_legacy(module=module, version=version) + if module is None: module = get_current_module_name() @@ -427,6 +587,24 @@ def run_request(): return _ThreadedRpc(target=run_request) +def start_version_async_legacy(module, version): + def _ResultHook(rpc): + mapped_errors = [ + modules_service_pb2.ModulesServiceError.INVALID_VERSION, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR + ] + expected_errors = { + modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE: + 'The specified module: %s, version: %s is already started.' % + (module, version) + } + _CheckAsyncResult(rpc, mapped_errors, expected_errors) + + request = modules_service_pb2.StartModuleRequest() + request.module = module + request.version = version + response = modules_service_pb2.StartModuleResponse() + return _MakeAsyncCall('StartModule', request, response, _ResultHook) def stop_version( module=None, @@ -459,6 +637,10 @@ def stop_version_async( Returns: A `UserRPC` to stop all instances for the given module version. """ + + if not _has_opted_in(): + return stop_version_async_legacy(module=module, version=version) + if module is None: module = get_current_module_name() @@ -477,6 +659,28 @@ def run_request(): return _ThreadedRpc(target=run_request) +def stop_version_async_legacy(module, version): + def _ResultHook(rpc): + mapped_errors = [ + modules_service_pb2.ModulesServiceError.INVALID_VERSION, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR + ] + expected_errors = { + modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE: + 'The specified module: %s, version: %s is already stopped.' % + (module, version) + } + _CheckAsyncResult(rpc, mapped_errors, expected_errors) + + request = modules_service_pb2.StopModuleRequest() + if module: + request.module = module + if version: + request.version = version + response = modules_service_pb2.StopModuleResponse() + return _MakeAsyncCall('StopModule', request, response, _ResultHook) + + def _construct_hostname(*hostname_parts): """Constructs a hostname for the given module, version, and instance.""" return ".".join(hostname_parts) @@ -505,9 +709,9 @@ def get_hostname( InvalidInstancesError: if the given instance value is invalid. TypeError: if the given instance type is invalid. """ - # - # START FIX: Validate instance parameter at the beginning of the function. - # + if not _has_opted_in(): + return get_hostname_legacy(module=module, version=version, instance=instance) + if instance is not None: try: instance_id = int(instance) @@ -515,23 +719,18 @@ def get_hostname( raise ValueError except (ValueError, TypeError) as e: raise InvalidInstancesError("Instance must be a non-negative integer.") from e - # - # END FIX - # project_id = _get_project_id() - client = discovery.build('appengine', 'v1') + req_module = module or get_current_module_name() - # If version is not specified, we will use the version of the current context. req_version = version or get_current_version_name() try: - # Get the application's services to check for the legacy "no-engine" case. services = get_modules() - - # Get the application's default hostname + client = _get_admin_api_client_with_useragent('get_hostname') request = client.apps().get(appsId=project_id) + response = request.execute() default_hostname = response.get('defaultHostname') @@ -547,15 +746,12 @@ def get_hostname( return _construct_hostname(instance, req_version, default_hostname) return _construct_hostname(req_version, default_hostname) - # --- Cases for modern applications with one or more services --- - if instance is not None: - # Request for a specific instance try: # Get version details to check scaling and instance count - request = client.apps().services().versions().get( + version_request = discovery.build('appengine', 'v1').apps().services().versions().get( appsId=project_id, servicesId=req_module, versionsId=req_version, view='FULL') - version_details = request.execute() + version_details = version_request.execute() if 'manualScaling' not in version_details: raise InvalidInstancesError( @@ -599,3 +795,25 @@ def get_hostname( # Request with a version but no instance return _construct_hostname(version, req_module, default_hostname) +def get_hostname_legacy(module, version, instance): + def _ResultHook(rpc): + mapped_errors = [ + modules_service_pb2.ModulesServiceError.INVALID_MODULE, + modules_service_pb2.ModulesServiceError.INVALID_INSTANCES + ] + _CheckAsyncResult(rpc, mapped_errors, []) + return rpc.response.hostname + + request = modules_service_pb2.GetHostnameRequest() + if module: + request.module = module + if version: + request.version = version + if instance or instance == 0: + if not isinstance(instance, (six.string_types, six.integer_types)): + raise TypeError("'instance' arg must be of type basestring, long or int.") + request.instance = str(instance) + response = modules_service_pb2.GetHostnameResponse() + return _MakeAsyncCall('GetHostname', request, response, + _ResultHook).get_result() + diff --git a/tests/google/appengine/api/modules/modules_test.py b/tests/google/appengine/api/modules/modules_test.py index b41572f..a517805 100755 --- a/tests/google/appengine/api/modules/modules_test.py +++ b/tests/google/appengine/api/modules/modules_test.py @@ -16,10 +16,18 @@ # """Tests for google.appengine.api.modules.""" +import logging import os +import google + from google.appengine.api.modules import modules +from google.appengine.api.modules import modules_service_pb2 +from google.appengine.runtime import apiproxy_errors from google.appengine.runtime.context import ctx_test_util +import google.auth +import google_auth_httplib2 +from googleapiclient import discovery import mox from absl.testing import absltest @@ -111,14 +119,33 @@ def testGetCurrentInstanceId_None(self): del os.environ['INSTANCE_ID'] self.assertIsNone(modules.get_current_instance_id()) - # --- Tests for get_modules --- + def SetSuccessExpectations(self, method, expected_request, service_response): + rpc = MockRpc(method, expected_request, service_response) + self.mox.StubOutWithMock(modules, '_GetRpc') + modules._GetRpc().AndReturn(rpc) + self.mox.ReplayAll() + + def SetExceptionExpectations(self, method, expected_request, + application_error_number): + rpc = MockRpc(method, expected_request, None, application_error_number) + self.mox.StubOutWithMock(modules, '_GetRpc') + modules._GetRpc().AndReturn(rpc) + self.mox.ReplayAll() + + # --- Tests for updated get_modules --- def testGetModules(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_modules').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.list(appsId='project').AndReturn(mock_request) mock_request.execute().AndReturn( @@ -127,11 +154,17 @@ def testGetModules(self): self.assertEqual(['module1', 'default'], modules.get_modules()) def testGetModules_InvalidProject(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_modules').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.list(appsId='project').AndReturn(mock_request) mock_request.execute().AndRaise(self._CreateHttpError(404)) @@ -139,15 +172,33 @@ def testGetModules_InvalidProject(self): with self.assertRaisesRegex(modules.Error, "Project 'project' not found."): modules.get_modules() - # --- Tests for get_versions --- + # --- Tests for legacy get_modules --- + + def testGetModulesLegacy(self): + """Test we return the expected results.""" + service_response = modules_service_pb2.GetModulesResponse() + service_response.module.append('module1') + service_response.module.append('module2') + self.SetSuccessExpectations('GetModules', + modules_service_pb2.GetModulesRequest(), + service_response) + self.assertEqual(['module1', 'module2'], modules.get_modules()) + + # --- Tests for updated get_versions --- def testGetVersions(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_versions').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.list( @@ -158,12 +209,17 @@ def testGetVersions(self): self.assertEqual(['v1', 'v2'], modules.get_versions()) def testGetVersions_InvalidModule(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_versions').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.list( @@ -173,29 +229,87 @@ def testGetVersions_InvalidModule(self): with self.assertRaisesRegex(modules.InvalidModuleError, "Module 'foo' not found."): modules.get_versions(module='foo') + + # --- Tests for Legacy get_versions --- + + def testGetVersionsLegacy(self): + """Test we return the expected results.""" + expected_request = modules_service_pb2.GetVersionsRequest() + expected_request.module = 'module1' + service_response = modules_service_pb2.GetVersionsResponse() + service_response.version.append('v1') + service_response.version.append('v2') + self.SetSuccessExpectations('GetVersions', + expected_request, + service_response) + self.assertEqual(['v1', 'v2'], modules.get_versions('module1')) + + def testGetVersionsLegacy_NoModule(self): + """Test we return the expected results when no module is passed.""" + expected_request = modules_service_pb2.GetVersionsRequest() + service_response = modules_service_pb2.GetVersionsResponse() + service_response.version.append('v1') + service_response.version.append('v2') + self.SetSuccessExpectations('GetVersions', + expected_request, + service_response) + self.assertEqual(['v1', 'v2'], modules.get_versions()) + + def testGetVersionsLegacy_InvalidModuleError(self): + """Test we raise the right error when the given module is invalid.""" + self.SetExceptionExpectations( + 'GetVersions', modules_service_pb2.GetVersionsRequest(), + modules_service_pb2.ModulesServiceError.INVALID_MODULE) + self.assertRaises(modules.InvalidModuleError, modules.get_versions) + + def testGetVersionsLegacy_TransientError(self): + """Test we raise the right error when a transient error is encountered.""" + self.SetExceptionExpectations( + 'GetVersions', modules_service_pb2.GetVersionsRequest(), + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) + self.assertRaises(modules.TransientError, modules.get_versions) - # --- Tests for get_default_version --- + # --- Tests for updated get_default_version --- def testGetDefaultVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + + mock_admin_api_client = self.mox.CreateMockAnything() + + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent( + 'get_default_version').AndReturn(mock_admin_api_client) + + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + + mock_admin_api_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.get(appsId='project', servicesId='default').AndReturn(mock_request) mock_request.execute().AndReturn( {'split': {'allocations': {'v1': 0.5, 'v2': 0.5}}}) + self.mox.ReplayAll() + + # The assertion remains the same self.assertEqual('v1', modules.get_default_version()) def testGetDefaultVersion_Lexicographical(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_admin_api_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent( + 'get_default_version').AndReturn(mock_admin_api_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_admin_api_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.get(appsId='project', servicesId='default').AndReturn(mock_request) @@ -204,45 +318,113 @@ def testGetDefaultVersion_Lexicographical(self): self.mox.ReplayAll() self.assertEqual('v1-stable', modules.get_default_version()) + def testGetDefaultVersion_NoDefaultVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_admin_api_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent( + 'get_default_version').AndReturn(mock_admin_api_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_admin_api_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.get(appsId='project', servicesId='default').AndReturn(mock_request) - mock_request.execute().AndReturn({}) # No split allocations + mock_request.execute().AndReturn({}) self.mox.ReplayAll() with self.assertRaisesRegex(modules.InvalidVersionError, 'Could not determine default version'): modules.get_default_version() def testGetDefaultVersion_InvalidModule(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + + mock_admin_api_client = self.mox.CreateMockAnything() + + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent( + 'get_default_version').AndReturn(mock_admin_api_client) + + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + + mock_admin_api_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.get(appsId='project', servicesId='foo').AndReturn(mock_request) + mock_request.execute().AndRaise(self._CreateHttpError(404)) + self.mox.ReplayAll() + with self.assertRaisesRegex(modules.InvalidModuleError, "Module 'foo' not found."): modules.get_default_version(module='foo') + + # --- Tests for legacy get_default_version --- + + def testGetDefaultVersionLegacy(self): + """Test we return the expected results.""" + expected_request = modules_service_pb2.GetDefaultVersionRequest() + expected_request.module = 'module1' + service_response = modules_service_pb2.GetDefaultVersionResponse() + service_response.version = 'v1' + self.SetSuccessExpectations('GetDefaultVersion', + expected_request, + service_response) + self.assertEqual('v1', modules.get_default_version('module1')) + + def testGetDefaultVersionLegacy_NoModule(self): + """Test we return the expected results when no module is passed.""" + expected_request = modules_service_pb2.GetDefaultVersionRequest() + service_response = modules_service_pb2.GetDefaultVersionResponse() + service_response.version = 'v1' + self.SetSuccessExpectations('GetDefaultVersion', + expected_request, + service_response) + self.assertEqual('v1', modules.get_default_version()) - # --- Tests for get_num_instances --- + def testGetDefaultVersionLegacy_InvalidModuleError(self): + """Test we raise an error when one is received from the lower API.""" + self.SetExceptionExpectations( + 'GetDefaultVersion', modules_service_pb2.GetDefaultVersionRequest(), + modules_service_pb2.ModulesServiceError.INVALID_MODULE) + self.assertRaises(modules.InvalidModuleError, modules.get_default_version) + + def testGetDefaultVersionLegacy_InvalidVersionError(self): + """Test we raise an error when one is received from the lower API.""" + self.SetExceptionExpectations( + 'GetDefaultVersion', modules_service_pb2.GetDefaultVersionRequest(), + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + self.assertRaises(modules.InvalidVersionError, modules.get_default_version) + + # --- Tests for updated get_num_instances --- def testGetNumInstances(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_num_instances').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.get(appsId='project', servicesId='default', @@ -252,12 +434,22 @@ def testGetNumInstances(self): self.assertEqual(5, modules.get_num_instances()) def testGetNumInstances_NoManualScaling(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_num_instances').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.get(appsId='project', servicesId='default', @@ -267,12 +459,20 @@ def testGetNumInstances_NoManualScaling(self): self.assertEqual(0, modules.get_num_instances()) def testGetNumInstances_InvalidVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_num_instances').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.get(appsId='project', servicesId='default', @@ -282,15 +482,82 @@ def testGetNumInstances_InvalidVersion(self): with self.assertRaises(modules.InvalidVersionError): modules.get_num_instances(version='v-bad') - # --- Tests for async operations (set_num_instances, start/stop_version) --- + # --- Tests for updated get_num_instances --- + + def testGetNumInstancesLegacy(self): + """Test we return the expected results.""" + expected_request = modules_service_pb2.GetNumInstancesRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + service_response = modules_service_pb2.GetNumInstancesResponse() + service_response.instances = 11 + self.SetSuccessExpectations('GetNumInstances', + expected_request, + service_response) + self.assertEqual(11, modules.get_num_instances('module1', 'v1')) + + def testGetNumInstancesLegacy_NoVersion(self): + """Test we return the expected results when no version is passed.""" + expected_request = modules_service_pb2.GetNumInstancesRequest() + expected_request.module = 'module1' + service_response = modules_service_pb2.GetNumInstancesResponse() + service_response.instances = 11 + self.SetSuccessExpectations('GetNumInstances', + expected_request, + service_response) + self.assertEqual(11, modules.get_num_instances('module1')) + + def testGetNumInstancesLegacy_NoModule(self): + """Test we return the expected results when no module is passed.""" + expected_request = modules_service_pb2.GetNumInstancesRequest() + expected_request.version = 'v1' + service_response = modules_service_pb2.GetNumInstancesResponse() + service_response.instances = 11 + self.SetSuccessExpectations('GetNumInstances', + expected_request, + service_response) + self.assertEqual(11, modules.get_num_instances(version='v1')) + + def testGetNumInstancesLegacy_AllDefaults(self): + """Test we return the expected results when no args are passed.""" + expected_request = modules_service_pb2.GetNumInstancesRequest() + service_response = modules_service_pb2.GetNumInstancesResponse() + service_response.instances = 11 + self.SetSuccessExpectations('GetNumInstances', + expected_request, + service_response) + self.assertEqual(11, modules.get_num_instances()) + + def testGetNumInstancesLegacy_InvalidVersionError(self): + """Test we raise the expected error when the API call fails.""" + expected_request = modules_service_pb2.GetNumInstancesRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'GetNumInstances', expected_request, + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + self.assertRaises(modules.InvalidVersionError, + modules.get_num_instances, 'module1', 'v1') + + # --- Tests for updated set_num_instances--- def testSetNumInstances(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('set_num_instances').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -304,16 +571,27 @@ def testSetNumInstances(self): modules.set_num_instances(10) def testSetNumInstances_TypeError(self): + os.environ['OPT_IN_MODULES'] = 'true' with self.assertRaises(TypeError): modules.set_num_instances('not-an-int') def testSetNumInstances_InvalidInstancesError(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('set_num_instances').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -326,14 +604,91 @@ def testSetNumInstances_InvalidInstancesError(self): self.mox.ReplayAll() with self.assertRaises(modules.InvalidInstancesError): modules.set_num_instances(-1) + + # --- Tests for legacy set_num_instances--- + + def testSetNumInstancesLegacy(self): + """Test we return the expected results.""" + expected_request = modules_service_pb2.SetNumInstancesRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + expected_request.instances = 12 + service_response = modules_service_pb2.SetNumInstancesResponse() + self.SetSuccessExpectations('SetNumInstances', + expected_request, + service_response) + modules.set_num_instances(12, 'module1', 'v1') + + def testSetNumInstancesLegacy_NoVersion(self): + """Test we return the expected results when no version is passed.""" + expected_request = modules_service_pb2.SetNumInstancesRequest() + expected_request.module = 'module1' + expected_request.instances = 13 + service_response = modules_service_pb2.SetNumInstancesResponse() + self.SetSuccessExpectations('SetNumInstances', + expected_request, + service_response) + modules.set_num_instances(13, 'module1') + + def testSetNumInstancesLegacy_NoModule(self): + """Test we return the expected results when no module is passed.""" + expected_request = modules_service_pb2.SetNumInstancesRequest() + expected_request.version = 'v1' + expected_request.instances = 14 + service_response = modules_service_pb2.SetNumInstancesResponse() + self.SetSuccessExpectations('SetNumInstances', + expected_request, + service_response) + modules.set_num_instances(14, version='v1') + + def testSetNumInstancesLegacy_AllDefaults(self): + """Test we return the expected results when no args are passed.""" + expected_request = modules_service_pb2.SetNumInstancesRequest() + expected_request.instances = 15 + service_response = modules_service_pb2.SetNumInstancesResponse() + self.SetSuccessExpectations('SetNumInstances', + expected_request, + service_response) + modules.set_num_instances(15) + + def testSetNumInstancesLegacy_BadInstancesType(self): + """Test we raise an error when we receive a bad instances type.""" + self.assertRaises(TypeError, modules.set_num_instances, 'no good') + + def testSetNumInstancesLegacy_InvalidVersionError(self): + """Test we raise an error when we receive on from the underlying API.""" + expected_request = modules_service_pb2.SetNumInstancesRequest() + expected_request.instances = 23 + self.SetExceptionExpectations( + 'SetNumInstances', expected_request, + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + self.assertRaises(modules.InvalidVersionError, + modules.set_num_instances, 23) + + def testSetNumInstancesLegacy_TransientError(self): + """Test we raise an error when we receive on from the underlying API.""" + expected_request = modules_service_pb2.SetNumInstancesRequest() + expected_request.instances = 23 + self.SetExceptionExpectations( + 'SetNumInstances', expected_request, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) + self.assertRaises(modules.TransientError, modules.set_num_instances, 23) + + # --- Tests for updated start_version--- def testStartVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('start_version').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -347,12 +702,18 @@ def testStartVersion(self): modules.start_version('default', 'v1') def testStartVersion_InvalidVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('start_version').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -367,12 +728,22 @@ def testStartVersion_InvalidVersion(self): modules.start_version('default', 'v-bad') def testStartVersionAsync_NoneArgs(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('start_version').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -386,13 +757,77 @@ def testStartVersionAsync_NoneArgs(self): rpc = modules.start_version_async(None, None) rpc.get_result() + # --- Tests for legacy start_version--- + + def testStartVersionLegacy(self): + """Test we pass through the expected args.""" + expected_request = modules_service_pb2.StartModuleRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + service_response = modules_service_pb2.StartModuleResponse() + self.SetSuccessExpectations('StartModule', + expected_request, + service_response) + modules.start_version('module1', 'v1') + + def testStartVersionLegacy_InvalidVersionError(self): + """Test we raise an error when we receive one from the API.""" + expected_request = modules_service_pb2.StartModuleRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'StartModule', expected_request, + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + self.assertRaises(modules.InvalidVersionError, + modules.start_version, + 'module1', + 'v1') + + def testStartVersionLegacy_UnexpectedStateError(self): + """Test we don't raise an error if the version is already started.""" + expected_request = modules_service_pb2.StartModuleRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.mox.StubOutWithMock(logging, 'info') + logging.info('The specified module: module1, version: v1 is already ' + 'started.') + self.SetExceptionExpectations( + 'StartModule', expected_request, + modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE) + modules.start_version('module1', 'v1') + + def testStartVersionLegacy_TransientError(self): + """Test we raise an error when we receive one from the API.""" + expected_request = modules_service_pb2.StartModuleRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'StartModule', expected_request, + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) + self.assertRaises(modules.TransientError, + modules.start_version, + 'module1', + 'v1') + + # --- Tests for updated stop_version--- + def testStopVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('stop_version').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -406,12 +841,20 @@ def testStopVersion(self): modules.stop_version() def testStopVersion_InvalidVersion(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('stop_version').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -426,12 +869,22 @@ def testStopVersion_InvalidVersion(self): modules.stop_version(version='v-bad') def testStopVersion_TransientError(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('stop_version').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -445,13 +898,78 @@ def testStopVersion_TransientError(self): with self.assertRaises(modules.TransientError): modules.stop_version() + # --- Tests for legacy stop_version-- + + def testStopVersionLegacy_NoModule(self): + """Test we pass through the expected args.""" + expected_request = modules_service_pb2.StopModuleRequest() + expected_request.version = 'v1' + service_response = modules_service_pb2.StopModuleResponse() + self.SetSuccessExpectations('StopModule', + expected_request, + service_response) + modules.stop_version(version='v1') + + def testStopVersionLegacy_NoVersion(self): + """Test we pass through the expected args.""" + expected_request = modules_service_pb2.StopModuleRequest() + expected_request.module = 'module1' + service_response = modules_service_pb2.StopModuleResponse() + self.SetSuccessExpectations('StopModule', + expected_request, + service_response) + modules.stop_version('module1') + + def testStopVersionLegacy_InvalidVersionError(self): + """Test we raise an error when we receive one from the API.""" + expected_request = modules_service_pb2.StopModuleRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'StopModule', expected_request, + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + self.assertRaises(modules.InvalidVersionError, + modules.stop_version, + 'module1', + 'v1') + + def testStopVersionLegacy_AlreadyStopped(self): + """Test we don't raise an error if the version is already stopped.""" + expected_request = modules_service_pb2.StopModuleRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.mox.StubOutWithMock(logging, 'info') + logging.info('The specified module: module1, version: v1 is already ' + 'stopped.') + self.SetExceptionExpectations( + 'StopModule', expected_request, + modules_service_pb2.ModulesServiceError.UNEXPECTED_STATE) + modules.stop_version('module1', 'v1') + + def testStopVersionLegacy_TransientError(self): + """Test we raise an error when we receive one from the API.""" + self.SetExceptionExpectations( + 'StopModule', modules_service_pb2.StopModuleRequest(), + modules_service_pb2.ModulesServiceError.TRANSIENT_ERROR) + self.assertRaises(modules.TransientError, modules.stop_version) + def testRaiseError_Generic(self): - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent(mox.IsA(str)).AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() mock_request = self.mox.CreateMockAnything() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.patch( @@ -465,85 +983,112 @@ def testRaiseError_Generic(self): with self.assertRaises(modules.Error): modules.stop_version() - # --- Tests for get_hostname --- + # --- Tests for updated get_hostname --- def testGetHostname_WithVersion_NoInstance(self): """Tests the simple case with an explicit module and version.""" - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_admin_api_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent( + 'get_hostname').AndReturn(mock_admin_api_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') self.mox.StubOutWithMock(modules, 'get_modules') - + modules.get_modules().AndReturn(['default', 'other']) mock_apps = self.mox.CreateMockAnything() mock_get_request = self.mox.CreateMockAnything() - - self.mock_admin_api_client.apps().AndReturn(mock_apps) - modules.get_modules().AndReturn(['default', 'other']) + mock_admin_api_client.apps().AndReturn(mock_apps) mock_apps.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - self.mox.ReplayAll() self.assertEqual('v2.foo.project.appspot.com', modules.get_hostname(module='foo', version='v2')) def testGetHostname_Instance_Success(self): - """Tests a successful request for a specific instance.""" - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client_1 = self.mox.CreateMockAnything() + mock_client_2 = self.mox.CreateMockAnything() + + # Mock the two main dependencies of get_hostname + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent( + 'get_hostname').AndReturn(mock_client_1) + self.mox.StubOutWithMock(modules.discovery, 'build') + modules.discovery.build('appengine', 'v1').AndReturn(mock_client_2) + + # Mock the helper functions + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') self.mox.StubOutWithMock(modules, 'get_modules') + modules.get_modules().AndReturn(['default', 'other']) + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') - mock_apps = self.mox.CreateMockAnything() - mock_services = self.mox.CreateMockAnything() - mock_versions = self.mox.CreateMockAnything() + # Set up expectations for the first client call + mock_apps_1 = self.mox.CreateMockAnything() mock_get_request = self.mox.CreateMockAnything() - mock_version_request = self.mox.CreateMockAnything() - - # Expect first call to .apps() - self.mock_admin_api_client.apps().AndReturn(mock_apps) - modules.get_modules().AndReturn(['default', 'other']) - mock_apps.get(appsId='project').AndReturn(mock_get_request) + mock_client_1.apps().AndReturn(mock_apps_1) + mock_apps_1.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - # Expect second call to .apps() - self.mock_admin_api_client.apps().AndReturn(mock_apps) - mock_apps.services().AndReturn(mock_services) - mock_services.versions().AndReturn(mock_versions) - mock_versions.get( + # Set up expectations for the second client call + mock_apps_2 = self.mox.CreateMockAnything() + mock_services_2 = self.mox.CreateMockAnything() + mock_versions_2 = self.mox.CreateMockAnything() + mock_version_request = self.mox.CreateMockAnything() + mock_client_2.apps().AndReturn(mock_apps_2) + mock_apps_2.services().AndReturn(mock_services_2) + mock_services_2.versions().AndReturn(mock_versions_2) + mock_versions_2.get( appsId='project', servicesId='default', versionsId='v1', view='FULL').AndReturn(mock_version_request) mock_version_request.execute().AndReturn( {'manualScaling': {'instances': 5}}) self.mox.ReplayAll() + self.assertEqual('2.v1.default.project.appspot.com', modules.get_hostname(instance='2')) + def testGetHostname_Instance_NoManualScaling(self): - """Tests instance request for a service without manual scaling.""" - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client_1 = self.mox.CreateMockAnything() + mock_client_2 = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client_1) + self.mox.StubOutWithMock(modules.discovery, 'build') + modules.discovery.build('appengine', 'v1').AndReturn(mock_client_2) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') self.mox.StubOutWithMock(modules, 'get_modules') - - mock_apps = self.mox.CreateMockAnything() - mock_services = self.mox.CreateMockAnything() - mock_versions = self.mox.CreateMockAnything() - mock_get_request = self.mox.CreateMockAnything() - mock_version_request = self.mox.CreateMockAnything() - - # Expect first call to .apps() - self.mock_admin_api_client.apps().AndReturn(mock_apps) modules.get_modules().AndReturn(['default', 'other']) - mock_apps.get(appsId='project').AndReturn(mock_get_request) + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') + mock_apps_1 = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() + mock_client_1.apps().AndReturn(mock_apps_1) + mock_apps_1.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - - # Expect second call to .apps() - self.mock_admin_api_client.apps().AndReturn(mock_apps) - mock_apps.services().AndReturn(mock_services) - mock_services.versions().AndReturn(mock_versions) - mock_versions.get( + mock_apps_2 = self.mox.CreateMockAnything() + mock_services_2 = self.mox.CreateMockAnything() + mock_versions_2 = self.mox.CreateMockAnything() + mock_version_request = self.mox.CreateMockAnything() + mock_client_2.apps().AndReturn(mock_apps_2) + mock_apps_2.services().AndReturn(mock_services_2) + mock_services_2.versions().AndReturn(mock_versions_2) + mock_versions_2.get( appsId='project', servicesId='default', versionsId='v1', view='FULL').AndReturn(mock_version_request) mock_version_request.execute().AndReturn({'automaticScaling': {}}) - self.mox.ReplayAll() with self.assertRaisesRegex( modules.InvalidInstancesError, @@ -552,25 +1097,33 @@ def testGetHostname_Instance_NoManualScaling(self): modules.get_hostname(instance='1') def testGetHostname_Instance_OutOfBounds(self): - """Tests instance request where the instance ID is out of bounds.""" - self._SetupAdminApiMocks() - self.mox.StubOutWithMock(modules, 'get_modules') + os.environ['OPT_IN_MODULES'] = 'true' + mock_api_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(google.auth, 'default') + google.auth.default().AndReturn((None, 'project')) + self.mox.StubOutWithMock(modules.discovery, 'build') + modules.discovery.build('appengine', 'v1', http=mox.IsA(object)).AndReturn(mock_api_client) + modules.discovery.build('appengine', 'v1').AndReturn(mock_api_client) + + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') + self.mox.StubOutWithMock(modules, 'get_modules') + modules.get_modules().AndReturn(['default', 'other']) + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') mock_apps = self.mox.CreateMockAnything() + mock_get_request = self.mox.CreateMockAnything() mock_services = self.mox.CreateMockAnything() mock_versions = self.mox.CreateMockAnything() - mock_get_request = self.mox.CreateMockAnything() mock_version_request = self.mox.CreateMockAnything() - - # Expect first call to .apps() - self.mock_admin_api_client.apps().AndReturn(mock_apps) - modules.get_modules().AndReturn(['default', 'other']) + mock_api_client.apps().AndReturn(mock_apps) mock_apps.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - - # Expect second call to .apps() - self.mock_admin_api_client.apps().AndReturn(mock_apps) + mock_api_client.apps().AndReturn(mock_apps) mock_apps.services().AndReturn(mock_services) mock_services.versions().AndReturn(mock_versions) mock_versions.get( @@ -578,7 +1131,6 @@ def testGetHostname_Instance_OutOfBounds(self): view='FULL').AndReturn(mock_version_request) mock_version_request.execute().AndReturn( {'manualScaling': {'instances': 5}}) - self.mox.ReplayAll() with self.assertRaisesRegex( modules.InvalidInstancesError, @@ -587,8 +1139,7 @@ def testGetHostname_Instance_OutOfBounds(self): def testGetHostname_Instance_InvalidValue(self): """Tests instance request with an invalid non-integer instance value.""" - # This test is now simpler. Because the validation happens at the very - # top of get_hostname, no API calls are made, so no mocks are needed. + os.environ['OPT_IN_MODULES'] = 'true' with self.assertRaisesRegex( modules.InvalidInstancesError, 'Instance must be a non-negative integer.'): @@ -596,70 +1147,250 @@ def testGetHostname_Instance_InvalidValue(self): def testGetHostname_NoVersion_VersionExistsOnTarget(self): """Tests no-version call where the current version exists on the target.""" - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') self.mox.StubOutWithMock(modules, 'get_modules') + modules.get_modules().AndReturn(['default', 'module1']) + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') self.mox.StubOutWithMock(modules, 'get_versions') - + modules.get_versions(module='module1').AndReturn(['v1', 'v2']) mock_apps = self.mox.CreateMockAnything() mock_get_request = self.mox.CreateMockAnything() - - self.mock_admin_api_client.apps().AndReturn(mock_apps) - modules.get_modules().AndReturn(['default', 'module1']) + mock_client.apps().AndReturn(mock_apps) mock_apps.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - modules.get_versions(module='module1').AndReturn(['v1', 'v2']) - self.mox.ReplayAll() self.assertEqual('v1.module1.project.appspot.com', modules.get_hostname(module='module1')) def testGetHostname_NoVersion_VersionDoesNotExistOnTarget(self): """Tests no-version call where the current version is not on the target.""" - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') self.mox.StubOutWithMock(modules, 'get_modules') + modules.get_modules().AndReturn(['default', 'module1']) + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') self.mox.StubOutWithMock(modules, 'get_versions') - + modules.get_versions(module='module1').AndReturn(['v2', 'v3']) mock_apps = self.mox.CreateMockAnything() mock_get_request = self.mox.CreateMockAnything() - - self.mock_admin_api_client.apps().AndReturn(mock_apps) - modules.get_modules().AndReturn(['default', 'module1']) + mock_client.apps().AndReturn(mock_apps) mock_apps.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - modules.get_versions(module='module1').AndReturn(['v2', 'v3']) - self.mox.ReplayAll() self.assertEqual('module1.project.appspot.com', modules.get_hostname(module='module1')) def testGetHostname_LegacyApp_Success(self): """Tests a hostname request for a legacy app without engines.""" - self._SetupAdminApiMocks() + os.environ['OPT_IN_MODULES'] = 'true' + mock_client = self.mox.CreateMockAnything() + self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') + modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client) + self.mox.StubOutWithMock(modules, '_get_project_id') + modules._get_project_id().AndReturn('project') self.mox.StubOutWithMock(modules, 'get_modules') - + modules.get_modules().AndReturn(['default']) + self.mox.StubOutWithMock(modules, 'get_current_module_name') + modules.get_current_module_name().AndReturn('default') + self.mox.StubOutWithMock(modules, 'get_current_version_name') + modules.get_current_version_name().AndReturn('v1') mock_apps = self.mox.CreateMockAnything() mock_get_request = self.mox.CreateMockAnything() - - self.mock_admin_api_client.apps().AndReturn(mock_apps) - modules.get_modules().AndReturn(['default']) + mock_client.apps().AndReturn(mock_apps) mock_apps.get(appsId='project').AndReturn(mock_get_request) mock_get_request.execute().AndReturn( {'defaultHostname': 'project.appspot.com'}) - self.mox.ReplayAll() self.assertEqual('v1.project.appspot.com', modules.get_hostname()) def testGetHostname_LegacyApp_WithInstance(self): """Tests a legacy app request with an invalid non-integer instance.""" - # This test was incorrect. It should expect an error for instance='i'. - # Like the test above, no mocks are needed because it fails on validation - # before any API calls are made. + os.environ['OPT_IN_MODULES'] = 'true' with self.assertRaisesRegex( modules.InvalidInstancesError, 'Instance must be a non-negative integer.'): modules.get_hostname(instance='i') + + # --- Tests for Legacy get_hostname --- + + def testGetHostnameLegacy(self): + """Test we pass through the expected args.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + expected_request.instance = '3' + service_response = modules_service_pb2.GetHostnameResponse() + service_response.hostname = 'abc' + self.SetSuccessExpectations('GetHostname', + expected_request, + service_response) + self.assertEqual('abc', modules.get_hostname('module1', 'v1', '3')) + + def testGetHostnameLegacy_NoModule(self): + """Test we pass through the expected args when no module is specified.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.version = 'v1' + expected_request.instance = '3' + service_response = modules_service_pb2.GetHostnameResponse() + service_response.hostname = 'abc' + self.SetSuccessExpectations('GetHostname', + expected_request, + service_response) + self.assertEqual('abc', modules.get_hostname(version='v1', instance='3')) + + def testGetHostnameLegacy_NoVersion(self): + """Test we pass through the expected args when no version is specified.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.instance = '3' + service_response = modules_service_pb2.GetHostnameResponse() + service_response.hostname = 'abc' + self.SetSuccessExpectations('GetHostname', + expected_request, + service_response) + self.assertEqual('abc', + modules.get_hostname(module='module1', instance='3')) + + def testGetHostnameLegacy_IntInstance(self): + """Test we pass through the expected args when an int instance is given.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.instance = '3' + service_response = modules_service_pb2.GetHostnameResponse() + service_response.hostname = 'abc' + self.SetSuccessExpectations('GetHostname', + expected_request, + service_response) + self.assertEqual('abc', modules.get_hostname(module='module1', instance=3)) + + def testGetHostnameLegacy_InstanceZero(self): + """Test we pass through the expected args when instance zero is given.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.instance = '0' + service_response = modules_service_pb2.GetHostnameResponse() + service_response.hostname = 'abc' + self.SetSuccessExpectations('GetHostname', + expected_request, + service_response) + self.assertEqual('abc', modules.get_hostname(module='module1', instance=0)) + + def testGetHostnameLegacy_NoArgs(self): + """Test we pass through the expected args when none are given.""" + expected_request = modules_service_pb2.GetHostnameRequest() + service_response = modules_service_pb2.GetHostnameResponse() + service_response.hostname = 'abc' + self.SetSuccessExpectations('GetHostname', + expected_request, + service_response) + self.assertEqual('abc', modules.get_hostname()) + + def testGetHostnameLegacy_BadInstanceType(self): + """Test get_hostname throws a TypeError when passed a float for instance.""" + self.assertRaises(TypeError, + modules.get_hostname, + 'module1', + 'v1', + 1.2) + + def testGetHostnameLegacy_InvalidModuleError(self): + """Test we raise an error when we receive one from the API.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'GetHostname', expected_request, + modules_service_pb2.ModulesServiceError.INVALID_MODULE) + self.assertRaises(modules.InvalidModuleError, + modules.get_hostname, + 'module1', + 'v1') + + def testGetHostnameLegacy_InvalidInstancesError(self): + """Test we raise an error when we receive one from the API.""" + self.SetExceptionExpectations( + 'GetHostname', modules_service_pb2.GetHostnameRequest(), + modules_service_pb2.ModulesServiceError.INVALID_INSTANCES) + self.assertRaises(modules.InvalidInstancesError, modules.get_hostname) + + def testGetHostnameLegacy_UnKnownError(self): + """Test we raise an error when we receive one from the API.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'GetHostname', expected_request, 1099) + self.assertRaisesRegex(modules.Error, + 'ApplicationError: 1099', + modules.get_hostname, + 'module1', + 'v1') + + def testGetHostnameLegacy_UnMappedError(self): + """Test we raise an error when we receive one from the API.""" + expected_request = modules_service_pb2.GetHostnameRequest() + expected_request.module = 'module1' + expected_request.version = 'v1' + self.SetExceptionExpectations( + 'GetHostname', expected_request, + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + expected_message = 'ApplicationError: %s' % ( + modules_service_pb2.ModulesServiceError.INVALID_VERSION) + self.assertRaisesRegex(modules.Error, + expected_message, + modules.get_hostname, + 'module1', + 'v1') + +class MockRpc(object): + """Mock UserRPC class.""" + + def __init__(self, expected_method, expected_request, service_response=None, + application_error_number=None): + self._expected_method = expected_method + self._expected_request = expected_request + self._service_response = service_response + self._application_error_number = application_error_number + + def check_success(self): + self._check_success_called = True + if self._application_error_number is not None: + raise apiproxy_errors.ApplicationError(self._application_error_number) + self.response.CopyFrom(self._service_response) + + def get_result(self): + self._check_success_called = False + result = self._hook(self) + if not self._check_success_called: + raise AssertionError('The hook is expected to call check_success()') + return result + + def make_call(self, method, + request, response, get_result_hook=None, user_data=None): + self.method = method + if self._expected_method != method: + raise ValueError('expected method %s but got method %s' % + (self._expected_method, method)) + self.request = request + if self._expected_request != request: + raise ValueError('expected request %s but got request %s' % + (self._expected_request, request)) + self.response = response + self._hook = get_result_hook + self.user_data = user_data if __name__ == '__main__': absltest.main() From 17b06653a54f8c96ec0c08e939c7489804f017ed Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Fri, 19 Dec 2025 10:43:29 +0000 Subject: [PATCH 56/57] Adding admin API implementation for modules toggled by environment variable --- src/google/appengine/api/modules/modules.py | 16 +++++ .../appengine/api/modules/modules_test.py | 60 +++++++++---------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index af49129..a5437b4 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -1,3 +1,19 @@ +#!/usr/bin/env python +# +# Copyright 2007 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# """Exposes methods to control services (modules) and versions of an app. Services were formerly known as modules and the API methods still diff --git a/tests/google/appengine/api/modules/modules_test.py b/tests/google/appengine/api/modules/modules_test.py index a517805..7de738b 100755 --- a/tests/google/appengine/api/modules/modules_test.py +++ b/tests/google/appengine/api/modules/modules_test.py @@ -135,7 +135,7 @@ def SetExceptionExpectations(self, method, expected_request, # --- Tests for updated get_modules --- def testGetModules(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_modules').AndReturn(mock_client) @@ -154,7 +154,7 @@ def testGetModules(self): self.assertEqual(['module1', 'default'], modules.get_modules()) def testGetModules_InvalidProject(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_modules').AndReturn(mock_client) @@ -187,7 +187,7 @@ def testGetModulesLegacy(self): # --- Tests for updated get_versions --- def testGetVersions(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_versions').AndReturn(mock_client) @@ -209,7 +209,7 @@ def testGetVersions(self): self.assertEqual(['v1', 'v2'], modules.get_versions()) def testGetVersions_InvalidModule(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_versions').AndReturn(mock_client) @@ -272,7 +272,7 @@ def testGetVersionsLegacy_TransientError(self): # --- Tests for updated get_default_version --- def testGetDefaultVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_admin_api_client = self.mox.CreateMockAnything() @@ -299,7 +299,7 @@ def testGetDefaultVersion(self): self.assertEqual('v1', modules.get_default_version()) def testGetDefaultVersion_Lexicographical(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_admin_api_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent( @@ -320,7 +320,7 @@ def testGetDefaultVersion_Lexicographical(self): def testGetDefaultVersion_NoDefaultVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_admin_api_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent( @@ -341,7 +341,7 @@ def testGetDefaultVersion_NoDefaultVersion(self): modules.get_default_version() def testGetDefaultVersion_InvalidModule(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_admin_api_client = self.mox.CreateMockAnything() @@ -409,7 +409,7 @@ def testGetDefaultVersionLegacy_InvalidVersionError(self): # --- Tests for updated get_num_instances --- def testGetNumInstances(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_num_instances').AndReturn(mock_client) @@ -434,7 +434,7 @@ def testGetNumInstances(self): self.assertEqual(5, modules.get_num_instances()) def testGetNumInstances_NoManualScaling(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_num_instances').AndReturn(mock_client) @@ -459,7 +459,7 @@ def testGetNumInstances_NoManualScaling(self): self.assertEqual(0, modules.get_num_instances()) def testGetNumInstances_InvalidVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_num_instances').AndReturn(mock_client) @@ -542,7 +542,7 @@ def testGetNumInstancesLegacy_InvalidVersionError(self): # --- Tests for updated set_num_instances--- def testSetNumInstances(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('set_num_instances').AndReturn(mock_client) @@ -571,12 +571,12 @@ def testSetNumInstances(self): modules.set_num_instances(10) def testSetNumInstances_TypeError(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' with self.assertRaises(TypeError): modules.set_num_instances('not-an-int') def testSetNumInstances_InvalidInstancesError(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('set_num_instances').AndReturn(mock_client) @@ -677,7 +677,7 @@ def testSetNumInstancesLegacy_TransientError(self): # --- Tests for updated start_version--- def testStartVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('start_version').AndReturn(mock_client) @@ -702,7 +702,7 @@ def testStartVersion(self): modules.start_version('default', 'v1') def testStartVersion_InvalidVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('start_version').AndReturn(mock_client) @@ -728,7 +728,7 @@ def testStartVersion_InvalidVersion(self): modules.start_version('default', 'v-bad') def testStartVersionAsync_NoneArgs(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('start_version').AndReturn(mock_client) @@ -812,7 +812,7 @@ def testStartVersionLegacy_TransientError(self): # --- Tests for updated stop_version--- def testStopVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('stop_version').AndReturn(mock_client) @@ -841,7 +841,7 @@ def testStopVersion(self): modules.stop_version() def testStopVersion_InvalidVersion(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('stop_version').AndReturn(mock_client) @@ -869,7 +869,7 @@ def testStopVersion_InvalidVersion(self): modules.stop_version(version='v-bad') def testStopVersion_TransientError(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('stop_version').AndReturn(mock_client) @@ -954,7 +954,7 @@ def testStopVersionLegacy_TransientError(self): self.assertRaises(modules.TransientError, modules.stop_version) def testRaiseError_Generic(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent(mox.IsA(str)).AndReturn(mock_client) @@ -987,7 +987,7 @@ def testRaiseError_Generic(self): def testGetHostname_WithVersion_NoInstance(self): """Tests the simple case with an explicit module and version.""" - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_admin_api_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent( @@ -1007,7 +1007,7 @@ def testGetHostname_WithVersion_NoInstance(self): modules.get_hostname(module='foo', version='v2')) def testGetHostname_Instance_Success(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client_1 = self.mox.CreateMockAnything() mock_client_2 = self.mox.CreateMockAnything() @@ -1057,7 +1057,7 @@ def testGetHostname_Instance_Success(self): def testGetHostname_Instance_NoManualScaling(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client_1 = self.mox.CreateMockAnything() mock_client_2 = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') @@ -1097,7 +1097,7 @@ def testGetHostname_Instance_NoManualScaling(self): modules.get_hostname(instance='1') def testGetHostname_Instance_OutOfBounds(self): - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_api_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(google.auth, 'default') google.auth.default().AndReturn((None, 'project')) @@ -1139,7 +1139,7 @@ def testGetHostname_Instance_OutOfBounds(self): def testGetHostname_Instance_InvalidValue(self): """Tests instance request with an invalid non-integer instance value.""" - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' with self.assertRaisesRegex( modules.InvalidInstancesError, 'Instance must be a non-negative integer.'): @@ -1147,7 +1147,7 @@ def testGetHostname_Instance_InvalidValue(self): def testGetHostname_NoVersion_VersionExistsOnTarget(self): """Tests no-version call where the current version exists on the target.""" - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client) @@ -1171,7 +1171,7 @@ def testGetHostname_NoVersion_VersionExistsOnTarget(self): def testGetHostname_NoVersion_VersionDoesNotExistOnTarget(self): """Tests no-version call where the current version is not on the target.""" - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client) @@ -1195,7 +1195,7 @@ def testGetHostname_NoVersion_VersionDoesNotExistOnTarget(self): def testGetHostname_LegacyApp_Success(self): """Tests a hostname request for a legacy app without engines.""" - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' mock_client = self.mox.CreateMockAnything() self.mox.StubOutWithMock(modules, '_get_admin_api_client_with_useragent') modules._get_admin_api_client_with_useragent('get_hostname').AndReturn(mock_client) @@ -1218,7 +1218,7 @@ def testGetHostname_LegacyApp_Success(self): def testGetHostname_LegacyApp_WithInstance(self): """Tests a legacy app request with an invalid non-integer instance.""" - os.environ['OPT_IN_MODULES'] = 'true' + os.environ['MODULES_USE_ADMIN_API'] = 'true' with self.assertRaisesRegex( modules.InvalidInstancesError, 'Instance must be a non-negative integer.'): From 4cf40908e93cb49921e032512393300fd2dc901d Mon Sep 17 00:00:00 2001 From: Hrithik Gajera Date: Wed, 24 Dec 2025 10:31:12 +0000 Subject: [PATCH 57/57] Using appropriate env variable name --- src/google/appengine/api/modules/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/appengine/api/modules/modules.py b/src/google/appengine/api/modules/modules.py index a5437b4..5387a57 100755 --- a/src/google/appengine/api/modules/modules.py +++ b/src/google/appengine/api/modules/modules.py @@ -84,7 +84,7 @@ class TransientError(Error): """A transient error was encountered, retry the operation.""" def _has_opted_in(): - return (os.environ.get('OPT_IN_MODULES', 'false').lower() == 'true') + return (os.environ.get('MODULES_USE_ADMIN_API', 'false').lower() == 'true') def _raise_error(e): # Translate HTTP errors to the exceptions expected by the API