Repository: cloudfoundry-community/cf-python-client

Issue:
Imminent v2 api death and impact on client intialization
CF API V2 is about to be retired. I think the first cut of cf-deployment from January 2025 onwards will no longer have v2 enabled by default

https://lists.cloudfoundry.org/g/cf-dev/message/9487

We've disabled it in our deployment ahead of time to test and although the majority of the V3 API calls in this library are supported, the init of the `client` requires V2 API so it no can no longer initialize.

We also get errors like below, because the V2 API no longer responds   

```
 File "/usr/local/lib/python3.10/site-packages/cloudfoundry_client/client.py", line 210, in _get_info
    root_links["cloud_controller_v2"]["meta"]["version"],
KeyError: 'cloud_controller_v2'
```

Discussion:
@pburkholder: @thelangley Do you have a list of all the things that are prone to break with the death of V2? @antechrestos: @thelangley @pburkholder I've just made a PR #221  that allows starting a client without V2 endpoint.

The only thing that won't work will be 

* push operation yet it was experimental
* log streaming might not wor  (there is a `/v2/read`  in log streaming urk  yet base url of stream log exposed on [root api](https://v3-apidocs.cloudfoundry.org/version/3.184.0/index.html#global-api-root) is another endpoint so I think they are not linked )
* direct access without `v2` such as `client.organizations` as requested in #43 ; I think this should be removed but kept it for now

What do you think of the evolution?

If you're ok I will merge it and make a release@thelangley: Thank you so much. This looks ace, could a release please be made and we'll test it out for you?@antechrestos: @thelangley I've just released the [1.38.0](https://pypi.org/project/cloudfoundry-client/1.38.0/)@thelangley: Tested this out in our use case and it's working nicely.

Thank you @antechrestos, appreciate your hard work 

Solution patch:
diff --git a/cloudfoundry_client/client.py b/cloudfoundry_client/client.py
index 77cc556..7794741 100644
--- a/cloudfoundry_client/client.py
+++ b/cloudfoundry_client/client.py
@@ -52,18 +52,28 @@
 class Info:
     def __init__(
         self,
-        api_v2_version: str,
+        api_v2_url: str,
+        api_v3_url: str,
         authorization_endpoint: str,
         api_endpoint: str,
         doppler_endpoint: Optional[str],
         log_stream_endpoint: Optional[str],
     ):
-        self.api_v2_version = api_v2_version
+        self._api_v2_url = api_v2_url
+        self._api_v3_url = api_v3_url
         self.authorization_endpoint = authorization_endpoint
         self.api_endpoint = api_endpoint
         self.doppler_endpoint = doppler_endpoint
         self.log_stream_endpoint = log_stream_endpoint
 
+    @property
+    def api_v2_url(self) -> Optional[str]:
+        return self._api_v2_url
+
+    @property
+    def api_v3_url(self) -> Optional[str]:
+        return self._api_v3_url
+
 
 class NetworkingV1External(object):
     def __init__(self, target_endpoint: str, credential_manager: "CloudFoundryClient"):
@@ -83,18 +93,18 @@ def __init__(self, target_endpoint: str, credential_manager: "CloudFoundryClient
         self.service_plans = ServicePlanManagerV2(target_endpoint, credential_manager)
         # Default implementations
         self.event = EventManager(target_endpoint, credential_manager)
-        self.organizations = EntityManagerV2(target_endpoint, credential_manager, "/v2/organizations")
-        self.private_domains = EntityManagerV2(target_endpoint, credential_manager, "/v2/private_domains")
+        self.organizations = EntityManagerV2(target_endpoint, credential_manager, "/organizations")
+        self.private_domains = EntityManagerV2(target_endpoint, credential_manager, "/private_domains")
         self.routes = RouteManager(target_endpoint, credential_manager)
-        self.services = EntityManagerV2(target_endpoint, credential_manager, "/v2/services")
-        self.shared_domains = EntityManagerV2(target_endpoint, credential_manager, "/v2/shared_domains")
+        self.services = EntityManagerV2(target_endpoint, credential_manager, "/services")
+        self.shared_domains = EntityManagerV2(target_endpoint, credential_manager, "/shared_domains")
         self.spaces = SpaceManagerV2(target_endpoint, credential_manager)
-        self.stacks = EntityManagerV2(target_endpoint, credential_manager, "/v2/stacks")
+        self.stacks = EntityManagerV2(target_endpoint, credential_manager, "/stacks")
         self.user_provided_service_instances = EntityManagerV2(
-            target_endpoint, credential_manager, "/v2/user_provided_service_instances"
+            target_endpoint, credential_manager, "/user_provided_service_instances"
         )
-        self.security_groups = EntityManagerV2(target_endpoint, credential_manager, "/v2/security_groups")
-        self.users = EntityManagerV2(target_endpoint, credential_manager, "/v2/users")
+        self.security_groups = EntityManagerV2(target_endpoint, credential_manager, "/security_groups")
+        self.users = EntityManagerV2(target_endpoint, credential_manager, "/users")
         # Resources implementation used by push operation
         self.resources = ResourceManager(target_endpoint, credential_manager)
 
@@ -146,8 +156,6 @@ def __init__(self, target_endpoint: str, client_id: str = "cf", client_secret: s
         self.login_hint = kwargs.get("login_hint")
         target_endpoint_trimmed = target_endpoint.rstrip("/")
         info = self._get_info(target_endpoint_trimmed, proxy, verify=verify)
-        if not info.api_v2_version.startswith("2."):
-            raise AssertionError("Only version 2 is supported for now. Found %s" % info.api_v2_version)
         service_information = ServiceInformation(
             None, "%s/oauth/token" % info.authorization_endpoint, client_id, client_secret, [], verify
         )
@@ -156,8 +164,16 @@ def __init__(self, target_endpoint: str, client_id: str = "cf", client_secret: s
             proxies=proxy,
             user_agent=kwargs.get("user_agent", "cf-python-client")
         )
-        self.v2 = V2(target_endpoint_trimmed, self)
-        self.v3 = V3(target_endpoint_trimmed, self)
+        self._v2 = (
+            V2(info.api_v2_url, self)
+            if info.api_v2_url is not None
+            else None
+        )
+        self._v3 = (
+            V3(info.api_v3_url, self)
+            if info.api_v3_url is not None
+            else None
+        )
         self._doppler = (
             DopplerClient(
                 info.doppler_endpoint,
@@ -181,6 +197,18 @@ def __init__(self, target_endpoint: str, client_id: str = "cf", client_secret: s
         self.networking_v1_external = NetworkingV1External(target_endpoint_trimmed, self)
         self.info = info
 
+    @property
+    def v2(self) -> V2:
+        if self._v2 is None:
+            raise NotImplementedError("No V2 endpoint for this instance")
+        return self._v2
+
+    @property
+    def v3(self) -> V3:
+        if self._v3 is None:
+            raise NotImplementedError("No V3 endpoint for this instance")
+        return self._v3
+
     @property
     def doppler(self) -> DopplerClient:
         if self._doppler is None:
@@ -194,7 +222,6 @@ def rlpgateway(self):
         if self._rlpgateway is None:
             raise NotImplementedError("No RLP gateway endpoint for this instance")
         else:
-
             return self._rlpgateway
 
     def _get_info(self, target_endpoint: str, proxy: Optional[dict] = None, verify: bool = True) -> Info:
@@ -206,8 +233,11 @@ def _get_info(self, target_endpoint: str, proxy: Optional[dict] = None, verify:
         root_links = root_info["links"]
         logging = root_links.get("logging")
         log_stream = root_links.get("log_stream")
+        cloud_controller_v2 = root_links.get("cloud_controller_v2")
+        cloud_controller_v3 = root_links.get("cloud_controller_v3")
         return Info(
-            root_links["cloud_controller_v2"]["meta"]["version"],
+            cloud_controller_v2["href"] if cloud_controller_v2 is not None else None,
+            cloud_controller_v3["href"] if cloud_controller_v3 is not None else None,
             self._resolve_login_endpoint(root_links),
             target_endpoint,
             logging.get("href") if logging is not None else None,
diff --git a/cloudfoundry_client/common_objects.py b/cloudfoundry_client/common_objects.py
index 9c3aa0f..36ef6ea 100644
--- a/cloudfoundry_client/common_objects.py
+++ b/cloudfoundry_client/common_objects.py
@@ -1,5 +1,5 @@
 import json
-from typing import Callable, TypeVar, Generic, List, Union
+from typing import Callable, TypeVar, Generic, List, Optional
 
 
 class Request(dict):
@@ -21,7 +21,7 @@ def __init__(self, *args, **kwargs):
 class Pagination(Generic[ENTITY]):
     def __init__(self, first_page: JsonObject,
                  total_result: int,
-                 next_page_loader: Callable[[JsonObject], Union[None, JsonObject]],
+                 next_page_loader: Callable[[JsonObject], Optional[JsonObject]],
                  resources_accessor: Callable[[JsonObject], List[JsonObject]],
                  instance_creator: Callable[[JsonObject], ENTITY]):
         self._first_page = first_page
diff --git a/cloudfoundry_client/v2/apps.py b/cloudfoundry_client/v2/apps.py
index cf68ed8..d060bff 100644
--- a/cloudfoundry_client/v2/apps.py
+++ b/cloudfoundry_client/v2/apps.py
@@ -77,7 +77,7 @@ class AppManager(EntityManager):
 
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
         super(AppManager, self).__init__(
-            target_endpoint, client, "/v2/apps", lambda pairs: Application(target_endpoint, client, pairs)
+            target_endpoint, client, "/apps", lambda pairs: Application(target_endpoint, client, pairs)
         )
 
     def get_stats(self, application_guid: str) -> Dict[str, JsonObject]:
diff --git a/cloudfoundry_client/v2/buildpacks.py b/cloudfoundry_client/v2/buildpacks.py
index 958a283..8e1276b 100644
--- a/cloudfoundry_client/v2/buildpacks.py
+++ b/cloudfoundry_client/v2/buildpacks.py
@@ -8,7 +8,7 @@
 
 class BuildpackManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(BuildpackManager, self).__init__(target_endpoint, client, "/v2/buildpacks")
+        super(BuildpackManager, self).__init__(target_endpoint, client, "/buildpacks")
 
     def update(self, buildpack_guid: str, parameters: dict) -> Entity:
         return super(BuildpackManager, self)._update(buildpack_guid, parameters)
diff --git a/cloudfoundry_client/v2/entities.py b/cloudfoundry_client/v2/entities.py
index d9ace97..e324d9e 100644
--- a/cloudfoundry_client/v2/entities.py
+++ b/cloudfoundry_client/v2/entities.py
@@ -1,5 +1,5 @@
 from functools import partial, reduce
-from typing import Callable, List, Tuple, Any, Optional, TYPE_CHECKING, Union
+from typing import Callable, List, Tuple, Any, Optional, TYPE_CHECKING
 from urllib.parse import quote
 from requests import Response
 
@@ -68,7 +68,7 @@ def _list(self, requested_path: str, entity_builder: Optional[EntityBuilder] = N
                           lambda page: page["resources"],
                           lambda json_object: current_builder(list(json_object.items())))
 
-    def _next_page(self, current_page: JsonObject) -> Union[None, JsonObject]:
+    def _next_page(self, current_page: JsonObject) -> Optional[JsonObject]:
         next_url = current_page.get("next_url")
         if next_url is None:
             return None
diff --git a/cloudfoundry_client/v2/events.py b/cloudfoundry_client/v2/events.py
index 9822165..cdec234 100644
--- a/cloudfoundry_client/v2/events.py
+++ b/cloudfoundry_client/v2/events.py
@@ -8,7 +8,7 @@
 
 class EventManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(EventManager, self).__init__(target_endpoint, client, "/v2/events")
+        super(EventManager, self).__init__(target_endpoint, client, "/events")
 
     def list_by_type(self, event_type: str) -> Generator[Entity, None, None]:
         return self._list(self.entity_uri, type=event_type)
diff --git a/cloudfoundry_client/v2/jobs.py b/cloudfoundry_client/v2/jobs.py
index 68bdaed..f1a6f53 100644
--- a/cloudfoundry_client/v2/jobs.py
+++ b/cloudfoundry_client/v2/jobs.py
@@ -12,4 +12,4 @@ def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
         self.client = client
 
     def get(self, job_guid: str) -> JsonObject:
-        return self.client.get("%s/v2/jobs/%s" % (self.target_endpoint, job_guid)).json(object_pairs_hook=JsonObject)
+        return self.client.get("%s/jobs/%s" % (self.target_endpoint, job_guid)).json(object_pairs_hook=JsonObject)
diff --git a/cloudfoundry_client/v2/resources.py b/cloudfoundry_client/v2/resources.py
index d962daa..2b0dae3 100644
--- a/cloudfoundry_client/v2/resources.py
+++ b/cloudfoundry_client/v2/resources.py
@@ -12,5 +12,5 @@ def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
         self.client = client
 
     def match(self, items: List[dict]) -> List[JsonObject]:
-        response = self.client.put("%s/v2/resource_match" % self.client.info.api_endpoint, json=items)
+        response = self.client.put("%s/resource_match" % self.client.info.api_endpoint, json=items)
         return response.json(object_pairs_hook=JsonObject)
diff --git a/cloudfoundry_client/v2/routes.py b/cloudfoundry_client/v2/routes.py
index 1e4d52c..bf9d425 100644
--- a/cloudfoundry_client/v2/routes.py
+++ b/cloudfoundry_client/v2/routes.py
@@ -8,7 +8,7 @@
 
 class RouteManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(RouteManager, self).__init__(target_endpoint, client, "/v2/routes")
+        super(RouteManager, self).__init__(target_endpoint, client, "/routes")
 
     def create_tcp_route(self, domain_guid: str, space_guid: str, port: Optional[int] = None) -> Entity:
         request = self._request(domain_guid=domain_guid, space_guid=space_guid)
diff --git a/cloudfoundry_client/v2/service_bindings.py b/cloudfoundry_client/v2/service_bindings.py
index 00fc625..95f9f5b 100644
--- a/cloudfoundry_client/v2/service_bindings.py
+++ b/cloudfoundry_client/v2/service_bindings.py
@@ -8,7 +8,7 @@
 
 class ServiceBindingManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceBindingManager, self).__init__(target_endpoint, client, "/v2/service_bindings")
+        super(ServiceBindingManager, self).__init__(target_endpoint, client, "/service_bindings")
 
     def create(self, app_guid: str, instance_guid: str, parameters: Optional[dict] = None, name: Optional[str] = None) -> Entity:
         request = self._request(app_guid=app_guid, service_instance_guid=instance_guid)
diff --git a/cloudfoundry_client/v2/service_brokers.py b/cloudfoundry_client/v2/service_brokers.py
index abe44c7..afc2c1a 100644
--- a/cloudfoundry_client/v2/service_brokers.py
+++ b/cloudfoundry_client/v2/service_brokers.py
@@ -8,7 +8,7 @@
 
 class ServiceBrokerManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceBrokerManager, self).__init__(target_endpoint, client, "/v2/service_brokers")
+        super(ServiceBrokerManager, self).__init__(target_endpoint, client, "/service_brokers")
 
     def create(
         self, broker_url: str, broker_name: str, auth_username: str, auth_password: str, space_guid: Optional[str] = None
diff --git a/cloudfoundry_client/v2/service_instances.py b/cloudfoundry_client/v2/service_instances.py
index f17fa7f..edeac74 100644
--- a/cloudfoundry_client/v2/service_instances.py
+++ b/cloudfoundry_client/v2/service_instances.py
@@ -10,7 +10,7 @@ class ServiceInstanceManager(EntityManager):
     list_query_parameters = ["page", "results-per-page", "order-direction", "return_user_provided_service_instances"]
 
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceInstanceManager, self).__init__(target_endpoint, client, "/v2/service_instances")
+        super(ServiceInstanceManager, self).__init__(target_endpoint, client, "/service_instances")
 
     def create(
         self,
diff --git a/cloudfoundry_client/v2/service_keys.py b/cloudfoundry_client/v2/service_keys.py
index cb87c3e..4f4da97 100644
--- a/cloudfoundry_client/v2/service_keys.py
+++ b/cloudfoundry_client/v2/service_keys.py
@@ -8,7 +8,7 @@
 
 class ServiceKeyManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceKeyManager, self).__init__(target_endpoint, client, "/v2/service_keys")
+        super(ServiceKeyManager, self).__init__(target_endpoint, client, "/service_keys")
 
     def create(self, service_instance_guid: str, name: str, parameters: Optional[dict] = None) -> Entity:
         request = self._request(service_instance_guid=service_instance_guid, name=name)
diff --git a/cloudfoundry_client/v2/service_plan_visibilities.py b/cloudfoundry_client/v2/service_plan_visibilities.py
index 4d7d513..47bcedf 100644
--- a/cloudfoundry_client/v2/service_plan_visibilities.py
+++ b/cloudfoundry_client/v2/service_plan_visibilities.py
@@ -8,7 +8,7 @@
 
 class ServicePlanVisibilityManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServicePlanVisibilityManager, self).__init__(target_endpoint, client, "/v2/service_plan_visibilities")
+        super(ServicePlanVisibilityManager, self).__init__(target_endpoint, client, "/service_plan_visibilities")
 
     def create(self, service_plan_guid: str, organization_guid: str) -> Entity:
         request = self._request()
diff --git a/cloudfoundry_client/v2/service_plans.py b/cloudfoundry_client/v2/service_plans.py
index 9a94aa8..8c8e766 100644
--- a/cloudfoundry_client/v2/service_plans.py
+++ b/cloudfoundry_client/v2/service_plans.py
@@ -9,7 +9,7 @@
 
 class ServicePlanManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServicePlanManager, self).__init__(target_endpoint, client, "/v2/service_plans")
+        super(ServicePlanManager, self).__init__(target_endpoint, client, "/service_plans")
 
     def create_from_resource_file(self, path: str) -> Entity:
         raise NotImplementedError("No creation allowed")
diff --git a/cloudfoundry_client/v2/spaces.py b/cloudfoundry_client/v2/spaces.py
index 6f6254a..b00743c 100644
--- a/cloudfoundry_client/v2/spaces.py
+++ b/cloudfoundry_client/v2/spaces.py
@@ -8,7 +8,7 @@
 
 class SpaceManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(SpaceManager, self).__init__(target_endpoint, client, "/v2/spaces")
+        super(SpaceManager, self).__init__(target_endpoint, client, "/spaces")
 
     def delete_unmapped_routes(self, space_guid: str):
         url = "%s%s/%s/unmapped_routes" % (self.target_endpoint, self.entity_uri, space_guid)
diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py
index 1453575..338c86c 100644
--- a/cloudfoundry_client/v3/apps.py
+++ b/cloudfoundry_client/v3/apps.py
@@ -9,7 +9,7 @@
 
 class AppManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(AppManager, self).__init__(target_endpoint, client, "/v3/apps")
+        super(AppManager, self).__init__(target_endpoint, client, "/apps")
 
     def restart(self, application_guid: str):
         return super(AppManager, self)._post("%s%s/%s/actions/restart" % (self.target_endpoint,
diff --git a/cloudfoundry_client/v3/buildpacks.py b/cloudfoundry_client/v3/buildpacks.py
index f6a1284..f8930b2 100644
--- a/cloudfoundry_client/v3/buildpacks.py
+++ b/cloudfoundry_client/v3/buildpacks.py
@@ -8,7 +8,7 @@
 
 class BuildpackManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(BuildpackManager, self).__init__(target_endpoint, client, "/v3/buildpacks")
+        super(BuildpackManager, self).__init__(target_endpoint, client, "/buildpacks")
 
     def create(
         self,
diff --git a/cloudfoundry_client/v3/domains.py b/cloudfoundry_client/v3/domains.py
index d1f5db3..0077115 100644
--- a/cloudfoundry_client/v3/domains.py
+++ b/cloudfoundry_client/v3/domains.py
@@ -21,7 +21,7 @@ def __init__(self, target_endpoint: str, client: "CloudFoundryClient", **kwargs)
 
 class DomainManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(DomainManager, self).__init__(target_endpoint, client, "/v3/domains", Domain)
+        super(DomainManager, self).__init__(target_endpoint, client, "/domains", Domain)
 
     def create(
         self,
@@ -44,7 +44,7 @@ def create(
         return super(DomainManager, self)._create(data)
 
     def list_domains_for_org(self, org_guid: str, **kwargs) -> Pagination[Entity]:
-        uri = "/v3/organizations/{guid}/domains".format(guid=org_guid)
+        uri = "/organizations/{guid}/domains".format(guid=org_guid)
         return self._list(uri, **kwargs)
 
     def update(self, domain_guid: str, meta_labels: Optional[dict] = None, meta_annotations: Optional[dict] = None) -> Domain:
diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py
index 36ec5f4..343a8f1 100644
--- a/cloudfoundry_client/v3/entities.py
+++ b/cloudfoundry_client/v3/entities.py
@@ -188,7 +188,7 @@ def _entity(json_object: JsonObject) -> Entity:
                           lambda p: p["resources"],
                           _entity)
 
-    def _next_page(self, current_page: JsonObject) -> Union[None, JsonObject]:
+    def _next_page(self, current_page: JsonObject) -> Optional[JsonObject]:
         pagination = current_page.get("pagination")
         if (
                 pagination is None
diff --git a/cloudfoundry_client/v3/feature_flags.py b/cloudfoundry_client/v3/feature_flags.py
index 47c35d8..3c280ad 100644
--- a/cloudfoundry_client/v3/feature_flags.py
+++ b/cloudfoundry_client/v3/feature_flags.py
@@ -8,7 +8,7 @@
 
 class FeatureFlagManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(FeatureFlagManager, self).__init__(target_endpoint, client, "/v3/feature_flags")
+        super(FeatureFlagManager, self).__init__(target_endpoint, client, "/feature_flags")
 
     def update(self, name: str, enabled: Optional[bool] = True, custom_error_message: Optional[str] = None) -> Entity:
         data = {"enabled": enabled, "custom_error_message": custom_error_message}
diff --git a/cloudfoundry_client/v3/isolation_segments.py b/cloudfoundry_client/v3/isolation_segments.py
index 67fa114..391e4d5 100644
--- a/cloudfoundry_client/v3/isolation_segments.py
+++ b/cloudfoundry_client/v3/isolation_segments.py
@@ -8,7 +8,7 @@
 
 class IsolationSegmentManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(IsolationSegmentManager, self).__init__(target_endpoint, client, "/v3/isolation_segments")
+        super(IsolationSegmentManager, self).__init__(target_endpoint, client, "/isolation_segments")
 
     def create(self, name: str, meta_labels: Optional[dict] = None, meta_annotations: Optional[dict] = None) -> Entity:
         data = {"name": name, "metadata": {"labels": meta_labels, "annotations": meta_annotations}}
diff --git a/cloudfoundry_client/v3/jobs.py b/cloudfoundry_client/v3/jobs.py
index 80bd510..5fd10fb 100644
--- a/cloudfoundry_client/v3/jobs.py
+++ b/cloudfoundry_client/v3/jobs.py
@@ -15,7 +15,7 @@ class JobTimeout(Exception):
 
 class JobManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(JobManager, self).__init__(target_endpoint, client, "/v3/jobs")
+        super(JobManager, self).__init__(target_endpoint, client, "/jobs")
 
     def wait_for_job_completion(
         self,
diff --git a/cloudfoundry_client/v3/organization_quotas.py b/cloudfoundry_client/v3/organization_quotas.py
index abe5015..2779f9c 100644
--- a/cloudfoundry_client/v3/organization_quotas.py
+++ b/cloudfoundry_client/v3/organization_quotas.py
@@ -35,7 +35,7 @@ class DomainsQuota:
 
 class OrganizationQuotaManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super().__init__(target_endpoint, client, "/v3/organization_quotas")
+        super().__init__(target_endpoint, client, "/organization_quotas")
 
     def remove(self, guid: str, asynchronous: bool = True) -> Optional[str]:
         return super()._remove(guid, asynchronous)
diff --git a/cloudfoundry_client/v3/organizations.py b/cloudfoundry_client/v3/organizations.py
index 2086d31..e7b4639 100644
--- a/cloudfoundry_client/v3/organizations.py
+++ b/cloudfoundry_client/v3/organizations.py
@@ -8,7 +8,7 @@
 
 class OrganizationManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(OrganizationManager, self).__init__(target_endpoint, client, "/v3/organizations")
+        super(OrganizationManager, self).__init__(target_endpoint, client, "/organizations")
 
     def create(
         self, name: str, suspended: bool, meta_labels: Optional[dict] = None, meta_annotations: Optional[dict] = None
diff --git a/cloudfoundry_client/v3/processes.py b/cloudfoundry_client/v3/processes.py
index cb632f3..f7ee291 100644
--- a/cloudfoundry_client/v3/processes.py
+++ b/cloudfoundry_client/v3/processes.py
@@ -8,4 +8,4 @@
 
 class ProcessManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ProcessManager, self).__init__(target_endpoint, client, "/v3/processes")
+        super(ProcessManager, self).__init__(target_endpoint, client, "/processes")
diff --git a/cloudfoundry_client/v3/roles.py b/cloudfoundry_client/v3/roles.py
index 9123d5a..ed466b9 100644
--- a/cloudfoundry_client/v3/roles.py
+++ b/cloudfoundry_client/v3/roles.py
@@ -8,7 +8,7 @@
 
 class RoleManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(RoleManager, self).__init__(target_endpoint, client, "/v3/roles")
+        super(RoleManager, self).__init__(target_endpoint, client, "/roles")
 
     def remove(self, role_guid: str, asynchronous: bool = True) -> Optional[str]:
         return super(RoleManager, self)._remove(role_guid, asynchronous)
diff --git a/cloudfoundry_client/v3/security_groups.py b/cloudfoundry_client/v3/security_groups.py
index b134e03..fdde7a2 100644
--- a/cloudfoundry_client/v3/security_groups.py
+++ b/cloudfoundry_client/v3/security_groups.py
@@ -37,7 +37,7 @@ class GloballyEnabled:
 
 class SecurityGroupManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(SecurityGroupManager, self).__init__(target_endpoint, client, "/v3/security_groups")
+        super(SecurityGroupManager, self).__init__(target_endpoint, client, "/security_groups")
 
     def create(self,
                name: str,
diff --git a/cloudfoundry_client/v3/service_brokers.py b/cloudfoundry_client/v3/service_brokers.py
index c39795b..a354de9 100644
--- a/cloudfoundry_client/v3/service_brokers.py
+++ b/cloudfoundry_client/v3/service_brokers.py
@@ -8,7 +8,7 @@
 
 class ServiceBrokerManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceBrokerManager, self).__init__(target_endpoint, client, "/v3/service_brokers")
+        super(ServiceBrokerManager, self).__init__(target_endpoint, client, "/service_brokers")
 
     def create(
         self,
diff --git a/cloudfoundry_client/v3/service_credential_bindings.py b/cloudfoundry_client/v3/service_credential_bindings.py
index d587b7a..6599686 100644
--- a/cloudfoundry_client/v3/service_credential_bindings.py
+++ b/cloudfoundry_client/v3/service_credential_bindings.py
@@ -9,7 +9,7 @@
 class ServiceCredentialBindingManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
         super(ServiceCredentialBindingManager, self).__init__(target_endpoint, client,
-                                                              "/v3/service_credential_bindings")
+                                                              "/service_credential_bindings")
 
     def create(
             self,
diff --git a/cloudfoundry_client/v3/service_instances.py b/cloudfoundry_client/v3/service_instances.py
index ad33614..695b0ef 100644
--- a/cloudfoundry_client/v3/service_instances.py
+++ b/cloudfoundry_client/v3/service_instances.py
@@ -9,7 +9,7 @@
 
 class ServiceInstanceManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceInstanceManager, self).__init__(target_endpoint, client, "/v3/service_instances")
+        super(ServiceInstanceManager, self).__init__(target_endpoint, client, "/service_instances")
 
     def create(
         self,
diff --git a/cloudfoundry_client/v3/service_offerings.py b/cloudfoundry_client/v3/service_offerings.py
index 37f7d0c..c784ca2 100644
--- a/cloudfoundry_client/v3/service_offerings.py
+++ b/cloudfoundry_client/v3/service_offerings.py
@@ -8,7 +8,7 @@
 
 class ServiceOfferingsManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServiceOfferingsManager, self).__init__(target_endpoint, client, "/v3/service_offerings")
+        super(ServiceOfferingsManager, self).__init__(target_endpoint, client, "/service_offerings")
 
     def update(self, guid: str, meta_labels: Optional[dict] = None, meta_annotations: Optional[dict] = None) -> Entity:
         payload = dict()
diff --git a/cloudfoundry_client/v3/service_plans.py b/cloudfoundry_client/v3/service_plans.py
index 257c3bf..9e27e15 100644
--- a/cloudfoundry_client/v3/service_plans.py
+++ b/cloudfoundry_client/v3/service_plans.py
@@ -8,7 +8,7 @@
 
 class ServicePlanManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(ServicePlanManager, self).__init__(target_endpoint, client, "/v3/service_plans")
+        super(ServicePlanManager, self).__init__(target_endpoint, client, "/service_plans")
 
     def update(
         self,
diff --git a/cloudfoundry_client/v3/spaces.py b/cloudfoundry_client/v3/spaces.py
index f871a43..acb3597 100644
--- a/cloudfoundry_client/v3/spaces.py
+++ b/cloudfoundry_client/v3/spaces.py
@@ -8,7 +8,7 @@
 
 class SpaceManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(SpaceManager, self).__init__(target_endpoint, client, "/v3/spaces")
+        super(SpaceManager, self).__init__(target_endpoint, client, "/spaces")
 
     def create(self, name: str, org_guid: str) -> Entity:
         return super(SpaceManager, self)._create(dict(name=name, relationships=dict(organization=ToOneRelationship(org_guid))))
diff --git a/cloudfoundry_client/v3/tasks.py b/cloudfoundry_client/v3/tasks.py
index a7ce16e..1367348 100644
--- a/cloudfoundry_client/v3/tasks.py
+++ b/cloudfoundry_client/v3/tasks.py
@@ -8,7 +8,7 @@
 
 class TaskManager(EntityManager):
     def __init__(self, target_endpoint: str, client: "CloudFoundryClient"):
-        super(TaskManager, self).__init__(target_endpoint, client, "/v3/tasks")
+        super(TaskManager, self).__init__(target_endpoint, client, "/tasks")
 
     def create(
         self,
@@ -24,7 +24,7 @@ def create(
         request["disk_in_mb"] = disk_in_mb
         request["memory_in_mb"] = memory_in_mb
         request["droplet_guid"] = droplet_guid
-        return self._post("%s/v3/apps/%s/tasks" % (self.target_endpoint, application_guid), data=request)
+        return self._post("%s/apps/%s/tasks" % (self.target_endpoint, application_guid), data=request)
 
     def cancel(self, task_guid: str) -> Entity:
-        return self._post("%s/v3/tasks/%s/actions/cancel" % (self.target_endpoint, task_guid))
+        return self._post("%s/tasks/%s/actions/cancel" % (self.target_endpoint, task_guid))


Test patch:
diff --git a/tests/abstract_test_case.py b/tests/abstract_test_case.py
index a672e0a..975fc2e 100644
--- a/tests/abstract_test_case.py
+++ b/tests/abstract_test_case.py
@@ -30,8 +30,6 @@ class AbstractTestCase(object):
     TOKEN_ENDPOINT = "http://token.somewhere.org"
     DOPPLER_ENDPOINT = "wss://doppler.nd-cfapi.itn.ftgroup:443"
     LOG_STREAM_ENDPOINT = "https://log-stream.nd-cfapi.itn.ftgroup"
-    API_V2_VERSION = "2.141.0"
-    API_V3_VERSION = "3.76.0"
 
     @classmethod
     def mock_client_class(cls):
@@ -43,33 +41,40 @@ def build_client(self):
             self.client = CloudFoundryClient(self.TARGET_ENDPOINT)
 
     @staticmethod
-    def _mock_info_calls(requests, with_doppler: bool = True, with_log_streams: bool = True):
+    def _mock_info_calls(
+            requests,
+            with_doppler: bool = True,
+            with_log_streams: bool = True,
+            with_v2: bool = True,
+            with_v3: bool = True
+    ):
+        links = {
+            "self": dict(href=AbstractTestCase.TARGET_ENDPOINT),
+            "cloud_controller_v2": dict(
+                href="%s/v2" % AbstractTestCase.TARGET_ENDPOINT,
+                meta=dict(version="2.141.0"),
+            ),
+            "cloud_controller_v3": dict(
+                href="%s/v3" % AbstractTestCase.TARGET_ENDPOINT,
+                meta=dict(version="3.76.0"),
+            ),
+            "logging": dict(href=AbstractTestCase.DOPPLER_ENDPOINT) if with_doppler else None,
+            "log_stream": dict(href=AbstractTestCase.LOG_STREAM_ENDPOINT) if with_log_streams else None,
+            "app_ssh": dict(href="ssh.nd-cfapi.itn.ftgroup:80"),
+            "uaa": dict(href="https://uaa.nd-cfapi.itn.ftgroup"),
+            "login": dict(href=AbstractTestCase.AUTHORIZATION_ENDPOINT),
+            "network_policy_v0": dict(href="https://api.nd-cfapi.itn.ftgroup/networking/v0/external"),
+            "network_policy_v1": dict(href="https://api.nd-cfapi.itn.ftgroup/networking/v1/external"),
+        }
+        if not with_v2:
+            del links["cloud_controller_v2"]
+        if not with_v3:
+            del links["cloud_controller_v3"]
         requests.get.side_effect = [
             MockResponse(
                 "%s/" % AbstractTestCase.TARGET_ENDPOINT,
                 status_code=HTTPStatus.OK.value,
-                text=json.dumps(
-                    dict(
-                        links={
-                            "self": dict(href=AbstractTestCase.TARGET_ENDPOINT),
-                            "cloud_controller_v2": dict(
-                                href="%s/v2" % AbstractTestCase.TARGET_ENDPOINT,
-                                meta=dict(version=AbstractTestCase.API_V2_VERSION),
-                            ),
-                            "cloud_controller_v3": dict(
-                                href="%s/v3" % AbstractTestCase.TARGET_ENDPOINT,
-                                meta=dict(version=AbstractTestCase.API_V3_VERSION),
-                            ),
-                            "logging": dict(href=AbstractTestCase.DOPPLER_ENDPOINT) if with_doppler else None,
-                            "log_stream": dict(href=AbstractTestCase.LOG_STREAM_ENDPOINT) if with_log_streams else None,
-                            "app_ssh": dict(href="ssh.nd-cfapi.itn.ftgroup:80"),
-                            "uaa": dict(href="https://uaa.nd-cfapi.itn.ftgroup"),
-                            "login": dict(href=AbstractTestCase.AUTHORIZATION_ENDPOINT),
-                            "network_policy_v0": dict(href="https://api.nd-cfapi.itn.ftgroup/networking/v0/external"),
-                            "network_policy_v1": dict(href="https://api.nd-cfapi.itn.ftgroup/networking/v1/external"),
-                        }
-                    )
-                ),
+                text=json.dumps(dict(links=links)),
             ),
         ]
 
diff --git a/tests/test_client.py b/tests/test_client.py
index b53d6de..1437d6e 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -42,6 +42,28 @@ def test_build_client_when_no_doppler(self):
             client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque")
             self.assertRaises(NotImplementedError, lambda: client.doppler)
 
+    def test_build_client_when_no_v2(self):
+        requests = FakeRequests()
+        session = MockSession()
+        with patch("oauth2_client.credentials_manager.requests", new=requests), patch(
+            "cloudfoundry_client.client.requests", new=requests
+        ):
+            requests.Session.return_value = session
+            self._mock_info_calls(requests, with_v2=False)
+            client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque")
+            self.assertRaises(NotImplementedError, lambda: client.v2)
+
+    def test_build_client_when_no_v3(self):
+        requests = FakeRequests()
+        session = MockSession()
+        with patch("oauth2_client.credentials_manager.requests", new=requests), patch(
+            "cloudfoundry_client.client.requests", new=requests
+        ):
+            requests.Session.return_value = session
+            self._mock_info_calls(requests, with_v3=False)
+            client = CloudFoundryClient(self.TARGET_ENDPOINT, token_format="opaque")
+            self.assertRaises(NotImplementedError, lambda: client.v3)
+
     def test_grant_password_request_with_token_format_opaque(self):
         requests = FakeRequests()
         session = MockSession()
@@ -162,7 +184,8 @@ def test_get_info(self):
             self._mock_info_calls(requests)
             info = client._get_info(self.TARGET_ENDPOINT)
             self.assertEqual(info.api_endpoint, self.TARGET_ENDPOINT)
-            self.assertEqual(info.api_v2_version, self.API_V2_VERSION)
+            self.assertEqual(info.api_v2_url, "%s/v2" % self.TARGET_ENDPOINT)
+            self.assertEqual(info.api_v3_url, "%s/v3" % self.TARGET_ENDPOINT)
             self.assertEqual(info.doppler_endpoint, self.DOPPLER_ENDPOINT)
             self.assertEqual(info.log_stream_endpoint, self.LOG_STREAM_ENDPOINT)