external-systems 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of external-systems might be problematic. Click here for more details.
- external_systems/__init__.py +15 -0
- external_systems/_version.py +19 -0
- external_systems/py.typed +0 -0
- external_systems/sources/__init__.py +38 -0
- external_systems/sources/_api.py +91 -0
- external_systems/sources/_connections.py +71 -0
- external_systems/sources/_proxies.py +134 -0
- external_systems/sources/_refreshable.py +130 -0
- external_systems/sources/_sockets.py +104 -0
- external_systems/sources/_sources.py +235 -0
- external_systems/sources/_utils.py +25 -0
- external_systems-0.1.0.dist-info/LICENSE.txt +13 -0
- external_systems-0.1.0.dist-info/METADATA +75 -0
- external_systems-0.1.0.dist-info/RECORD +15 -0
- external_systems-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from ._version import __version__ as __version__
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# The version is set during the publishing step (since we can't know the version in advance)
|
|
17
|
+
# using the autorelease bot
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from ._api import (
|
|
16
|
+
AwsCredentials,
|
|
17
|
+
ClientCertificate,
|
|
18
|
+
ClientCertificateFilePaths,
|
|
19
|
+
HttpsConnectionParameters,
|
|
20
|
+
SourceCredentials,
|
|
21
|
+
SourceParameters,
|
|
22
|
+
)
|
|
23
|
+
from ._connections import HttpsConnection
|
|
24
|
+
from ._refreshable import Refreshable, RefreshHandler
|
|
25
|
+
from ._sources import Source
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ClientCertificate",
|
|
29
|
+
"ClientCertificateFilePaths",
|
|
30
|
+
"HttpsConnection",
|
|
31
|
+
"HttpsConnectionParameters",
|
|
32
|
+
"Source",
|
|
33
|
+
"SourceParameters",
|
|
34
|
+
"RefreshHandler",
|
|
35
|
+
"Refreshable",
|
|
36
|
+
"AwsCredentials",
|
|
37
|
+
"SourceCredentials",
|
|
38
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Dict, Optional, Union
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ClientCertificate:
|
|
22
|
+
"""
|
|
23
|
+
A class representing a client certificate presentable to an external system for authentication
|
|
24
|
+
Attributes:
|
|
25
|
+
pem_certificate (str): PEM certificate contents
|
|
26
|
+
pem_private_key (str): PEM private key contents
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
pem_certificate: str
|
|
30
|
+
pem_private_key: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ClientCertificateFilePaths:
|
|
35
|
+
"""
|
|
36
|
+
A class holding the paths of the Source's client certificate
|
|
37
|
+
Attributes:
|
|
38
|
+
certificate_file_path (str): Path to the certificate file
|
|
39
|
+
private_key_file_path (str): Path to the private key file
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
certificate_file_path: str
|
|
43
|
+
private_key_file_path: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class HttpsConnectionParameters:
|
|
48
|
+
"""
|
|
49
|
+
A class representing HTTPS connection parameters for an external system.
|
|
50
|
+
Attributes:
|
|
51
|
+
url (str): The base URL of the external system.
|
|
52
|
+
headers (Dict[str, str]): A dictionary containing the auth headers for the connection.
|
|
53
|
+
query_params (Dict[str, str]): A dictionary containing the auth query parameters for the connection.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
url: str
|
|
57
|
+
headers: dict[str, str]
|
|
58
|
+
query_params: dict[str, str]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class AwsCredentials:
|
|
63
|
+
access_key_id: str
|
|
64
|
+
secret_access_key: str
|
|
65
|
+
session_token: Optional[str] = None
|
|
66
|
+
expiration: Optional[datetime] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Each new credential type must include an expiration field
|
|
70
|
+
SourceCredentials = Union[AwsCredentials]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class SourceParameters:
|
|
75
|
+
"""
|
|
76
|
+
A class representing source parameters for an external system.
|
|
77
|
+
Attributes:
|
|
78
|
+
secrets (Dict[str, str]): A dictionary containing the secrets contained on the source.
|
|
79
|
+
proxy_token (Optional[str]): The token for authenticating with the on-prem-proxy service.
|
|
80
|
+
https_connections (Dict[str, HttpsConnection]): A dictionary containing the https connections on the Source.
|
|
81
|
+
server_certificates (Dict[str, str]): A dictionary containing the server certificates for the source.
|
|
82
|
+
client_certificate Optional[ClientCertificate]: The client certificate configured on the source.
|
|
83
|
+
resolved_source_credentials (Optional[SourceCredentials]): If session credentials are used, this field will contain the resolved credentials.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
secrets: Dict[str, str]
|
|
87
|
+
proxy_token: Optional[str]
|
|
88
|
+
https_connections: Dict[str, HttpsConnectionParameters]
|
|
89
|
+
server_certificates: Dict[str, str]
|
|
90
|
+
client_certificate: Optional[ClientCertificate]
|
|
91
|
+
resolved_source_credentials: Optional[SourceCredentials]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Mapping, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from frozendict import frozendict
|
|
18
|
+
from requests import Session
|
|
19
|
+
|
|
20
|
+
from ._api import HttpsConnectionParameters
|
|
21
|
+
from ._proxies import create_session
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HttpsConnection:
|
|
25
|
+
"""
|
|
26
|
+
A class representing an HTTPS connection for an external system.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
connection_parameters: HttpsConnectionParameters,
|
|
32
|
+
client_certificate: Optional[Tuple[str, str]] = None,
|
|
33
|
+
proxy_uri_with_auth: Optional[str] = None,
|
|
34
|
+
ca_bundle_path: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
self._client_certificate = client_certificate
|
|
37
|
+
self._headers = frozendict(connection_parameters.headers)
|
|
38
|
+
self._url = connection_parameters.url
|
|
39
|
+
self._query_params = frozendict(connection_parameters.query_params)
|
|
40
|
+
self._proxies = frozendict({"https": proxy_uri_with_auth}) if proxy_uri_with_auth is not None else None
|
|
41
|
+
self._ca_bundle_path = ca_bundle_path
|
|
42
|
+
|
|
43
|
+
def get_client(self, timeout: int = 30) -> Session:
|
|
44
|
+
"""Get the HTTP client for this connection.
|
|
45
|
+
This client must be used in order to reach the external system.
|
|
46
|
+
Args:
|
|
47
|
+
timeout (int, optional): The request timeout in seconds. Defaults to 30.
|
|
48
|
+
Returns:
|
|
49
|
+
requests.Session: A configured requests.Session object for communicating with the external system.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
return create_session(
|
|
53
|
+
cert=self._client_certificate,
|
|
54
|
+
ca_bundle_path=self._ca_bundle_path,
|
|
55
|
+
headers=self._headers,
|
|
56
|
+
user_agent="external-systems",
|
|
57
|
+
proxies=self._proxies,
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def headers(self) -> Mapping[str, str]:
|
|
63
|
+
return self._headers
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def query_params(self) -> Mapping[str, str]:
|
|
67
|
+
return self._query_params
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def url(self) -> str:
|
|
71
|
+
return self._url
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from functools import cache
|
|
16
|
+
from typing import Any, Mapping, Optional, Union
|
|
17
|
+
|
|
18
|
+
from requests import PreparedRequest, Response, Session
|
|
19
|
+
from requests.adapters import HTTPAdapter
|
|
20
|
+
from urllib3.util.retry import Retry
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RetryingTimeoutHttpAdapter(HTTPAdapter):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*args: Any,
|
|
27
|
+
timeout: Optional[Union[float, tuple[float, float], tuple[float, None]]] = None,
|
|
28
|
+
retries: Optional[Union[Retry, int]] = None,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
):
|
|
31
|
+
self._timeout = timeout
|
|
32
|
+
retries = retries or Retry(total=10, backoff_factor=0.1, status_forcelist=[503])
|
|
33
|
+
super().__init__(*args, max_retries=retries, **kwargs) # type: ignore[misc]
|
|
34
|
+
|
|
35
|
+
def send(
|
|
36
|
+
self,
|
|
37
|
+
request: PreparedRequest,
|
|
38
|
+
stream: bool = False,
|
|
39
|
+
timeout: Optional[Union[float, tuple[float, float], tuple[float, None]]] = None,
|
|
40
|
+
verify: Union[bool, str] = True,
|
|
41
|
+
cert: Optional[Union[bytes, str, tuple[Union[bytes, str], Union[bytes, str]]]] = None,
|
|
42
|
+
proxies: Optional[Mapping[str, str]] = None,
|
|
43
|
+
) -> Response:
|
|
44
|
+
if timeout is None:
|
|
45
|
+
timeout = self._timeout
|
|
46
|
+
return super().send(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
|
|
47
|
+
|
|
48
|
+
def __getstate__(self) -> Any:
|
|
49
|
+
state: dict[str, Any] = super().__getstate__() # type: ignore
|
|
50
|
+
# The HTTPAdapter superclass overrides the __getstate__ method to serialize only selective fields.
|
|
51
|
+
# Any fields specific to this class must be added here so that these fields will be preserved after being
|
|
52
|
+
# pickled/unpickled.
|
|
53
|
+
state["_timeout"] = self._timeout
|
|
54
|
+
return state
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ProxyAdapter(HTTPAdapter):
|
|
58
|
+
def __init__(self, *args: Any, proxy_auth: dict[str, str], **kwargs: Any):
|
|
59
|
+
super().__init__(*args, **kwargs)
|
|
60
|
+
self._proxy_auth = proxy_auth
|
|
61
|
+
|
|
62
|
+
def proxy_headers(self, proxy: str) -> dict[str, str]:
|
|
63
|
+
return {"Proxy-Authorization": self._proxy_auth[proxy]}
|
|
64
|
+
|
|
65
|
+
def __getstate__(self) -> Any:
|
|
66
|
+
state: dict[str, Any] = super().__getstate__() # type: ignore
|
|
67
|
+
# The HTTPAdapter superclass overrides the __getstate__ method to serialize only selective fields.
|
|
68
|
+
# Any fields specific to this class must be added here so that these fields will be preserved after being
|
|
69
|
+
# pickled/unpickled.
|
|
70
|
+
state["_proxy_auth"] = self._proxy_auth
|
|
71
|
+
return state
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@cache
|
|
75
|
+
def create_proxy_session(proxy_url: str, proxy_token: str) -> Session:
|
|
76
|
+
"""
|
|
77
|
+
Create a session with proxy authentication.
|
|
78
|
+
Args:
|
|
79
|
+
proxy_url (str): The URL of the proxy server.
|
|
80
|
+
proxy_token (str): The token for authenticating with the proxy server.
|
|
81
|
+
Returns:
|
|
82
|
+
Session: A requests session with proxy authentication.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
adapter = ProxyAdapter(proxy_auth={proxy_url: f"Bearer {proxy_token}"})
|
|
86
|
+
session = Session()
|
|
87
|
+
session.mount("http://", adapter)
|
|
88
|
+
session.mount("https://", adapter)
|
|
89
|
+
session.proxies = {"all": proxy_url}
|
|
90
|
+
return session
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@cache
|
|
94
|
+
def create_session(
|
|
95
|
+
cert: Optional[Union[str, tuple[str, str]]] = None,
|
|
96
|
+
ca_bundle_path: Optional[str] = None,
|
|
97
|
+
headers: Optional[dict[str, str]] = None,
|
|
98
|
+
user_agent: Optional[str] = None,
|
|
99
|
+
proxies: Optional[dict[str, str]] = None,
|
|
100
|
+
timeout: Optional[Union[float, tuple[float, float], tuple[float, None]]] = None,
|
|
101
|
+
) -> Session:
|
|
102
|
+
"""
|
|
103
|
+
Create a session with optional client certificate and CA bundle path.
|
|
104
|
+
Args:
|
|
105
|
+
cert (Optional[Union[str, tuple[str, str]]]): The client certificate to use for authentication.
|
|
106
|
+
ca_bundle_path (Optional[str]): The path to the CA bundle file.
|
|
107
|
+
headers (Optional[dict[str, str]]): Additional headers to include in the request.
|
|
108
|
+
user_agent (Optional[str]): The User-Agent header value.
|
|
109
|
+
proxies (Optional[dict[str, str]]): Proxies to use for the session.
|
|
110
|
+
timeout (Optional[Union[float, tuple[float, float], tuple[float, None]]]): Timeout settings for the session.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
session = Session()
|
|
114
|
+
|
|
115
|
+
if cert:
|
|
116
|
+
session.cert = cert
|
|
117
|
+
|
|
118
|
+
if ca_bundle_path:
|
|
119
|
+
session.verify = ca_bundle_path
|
|
120
|
+
|
|
121
|
+
adapter = RetryingTimeoutHttpAdapter(timeout=timeout)
|
|
122
|
+
session.mount("http://", adapter)
|
|
123
|
+
session.mount("https://", adapter)
|
|
124
|
+
|
|
125
|
+
if headers:
|
|
126
|
+
session.headers.update(headers)
|
|
127
|
+
|
|
128
|
+
if user_agent:
|
|
129
|
+
session.headers["User-Agent"] = user_agent
|
|
130
|
+
|
|
131
|
+
if proxies:
|
|
132
|
+
session.proxies = proxies
|
|
133
|
+
|
|
134
|
+
return session
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from datetime import datetime, timedelta
|
|
18
|
+
from typing import Generic, Optional, TypeVar
|
|
19
|
+
|
|
20
|
+
from external_systems.sources._api import SourceCredentials
|
|
21
|
+
from external_systems.sources._utils import has_expiration_property
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
# Time before expiration to refresh the credentials
|
|
28
|
+
REFRESH_TIME_OFFSET_SECONDS = 5
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RefreshHandler(ABC, Generic[T]):
|
|
32
|
+
"""
|
|
33
|
+
This manages how and where to retrieve the new refreshed resource from
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def refresh(self) -> Optional[T]:
|
|
38
|
+
"""
|
|
39
|
+
Returns:
|
|
40
|
+
T: The refreshed resource.
|
|
41
|
+
"""
|
|
42
|
+
raise NotImplementedError("refresh() method not implemented")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Refreshable(ABC, Generic[T]):
|
|
46
|
+
"""
|
|
47
|
+
This manages the lifecycle of a resource that can be refreshed.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get(self) -> T:
|
|
52
|
+
"""
|
|
53
|
+
Retrieve the current value of the resource.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
T: The current value of the resource.
|
|
57
|
+
"""
|
|
58
|
+
raise NotImplementedError("get() method not implemented")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DefaultSessionCredentialsManager(Refreshable[SourceCredentials]):
|
|
62
|
+
"""
|
|
63
|
+
Client for refreshing resolved source credentials.
|
|
64
|
+
Will return credentials if they are not expired.
|
|
65
|
+
Otherwise will attempt to lazily refresh the credentials on `get()` calls.
|
|
66
|
+
|
|
67
|
+
:param source_credentials: SourceCredentials object returned by API representing the source credentials.
|
|
68
|
+
:param refresh_handler: A provider responsible for performing the refresh.
|
|
69
|
+
:param refresh_offset_seconds: Represents how many seconds before the credentials expire should we refresh them.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
resolved_source_credentials: SourceCredentials,
|
|
75
|
+
refresh_handler: Optional[RefreshHandler[SourceCredentials]] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
self._credentials: SourceCredentials = resolved_source_credentials
|
|
78
|
+
self._refresh_handler = refresh_handler
|
|
79
|
+
|
|
80
|
+
def get(self) -> SourceCredentials:
|
|
81
|
+
"""
|
|
82
|
+
Returns the source's credentials.
|
|
83
|
+
|
|
84
|
+
This method automatically refreshes the credentials if they are expired.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
SourceCredentials: The current source credentials.
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
ValueError: If the credentials have not been set.
|
|
91
|
+
"""
|
|
92
|
+
self._maybe_refresh_credentials()
|
|
93
|
+
if self._credentials is None:
|
|
94
|
+
raise ValueError("Credentials have not been set")
|
|
95
|
+
return self._credentials
|
|
96
|
+
|
|
97
|
+
def _maybe_refresh_credentials(self) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Checks if the credentials are expired and delegates the refresh to `_refresh_credentials` if needed.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# Don't refresh if expiration time is not set
|
|
103
|
+
if (
|
|
104
|
+
self._credentials is None
|
|
105
|
+
or not has_expiration_property(self._credentials)
|
|
106
|
+
or self._credentials.expiration is None
|
|
107
|
+
):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# If the credentials are valid for more than specified seconds, don't refresh
|
|
111
|
+
time_until_creds_expire = self._credentials.expiration - datetime.now()
|
|
112
|
+
time_until_creds_expire_with_offset = time_until_creds_expire - timedelta(seconds=REFRESH_TIME_OFFSET_SECONDS)
|
|
113
|
+
needs_refresh = time_until_creds_expire_with_offset.total_seconds() <= 0
|
|
114
|
+
|
|
115
|
+
if not needs_refresh:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
self._refresh_credentials()
|
|
119
|
+
|
|
120
|
+
def _refresh_credentials(self) -> None:
|
|
121
|
+
if self._refresh_handler is None:
|
|
122
|
+
# Refresh handler is not set, nothing to refresh
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
maybe_resolved_credentials = self._refresh_handler.refresh()
|
|
126
|
+
|
|
127
|
+
if maybe_resolved_credentials is None:
|
|
128
|
+
raise ValueError("Refresh failed for source")
|
|
129
|
+
|
|
130
|
+
self._credentials = maybe_resolved_credentials
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import socket
|
|
20
|
+
import ssl
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
import urllib3
|
|
24
|
+
|
|
25
|
+
log = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
STATUS_LINE_PATTERN = re.compile(r"^HTTP/\d\.\d (\d{3}) .*$")
|
|
29
|
+
NUM_RETRIES = 10
|
|
30
|
+
RETRYABLE_RESPONSE_CODES = {503, 429}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_socket(https_proxy_uri: str, target_host: str, target_port: int) -> socket.socket:
|
|
34
|
+
"""
|
|
35
|
+
Establishes a socket connection through an HTTPS proxy to a target host and port.
|
|
36
|
+
Parameters:
|
|
37
|
+
https_proxy_uri (str): The URI of the HTTPS proxy, must include auth if required.
|
|
38
|
+
target_host (str): The hostname of the target server to connect to.
|
|
39
|
+
target_port (int): The port number of the target server to connect to.
|
|
40
|
+
Returns:
|
|
41
|
+
socket.socket: A connected SSL socket to the target host and port through the proxy.
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If the proxy URI does not specify a hostname or port, or if the connection fails after retrying, with an invalid response code.
|
|
44
|
+
RuntimeError: If there is an exception during the socket creation process.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
parsed_proxy_uri = urllib3.util.parse_url(https_proxy_uri)
|
|
48
|
+
|
|
49
|
+
if parsed_proxy_uri.hostname is None:
|
|
50
|
+
raise ValueError("proxy uri has no hostname specified")
|
|
51
|
+
|
|
52
|
+
if parsed_proxy_uri.port is None:
|
|
53
|
+
raise ValueError("proxy uri has no port specified")
|
|
54
|
+
|
|
55
|
+
last_response_code = -1
|
|
56
|
+
for _ in range(NUM_RETRIES):
|
|
57
|
+
try:
|
|
58
|
+
proxy_socket = _create_ssl_socket(parsed_proxy_uri.hostname, parsed_proxy_uri.port)
|
|
59
|
+
proxy_socket.sendall(f"CONNECT {target_host}:{target_port} HTTP/1.1\r\n".encode())
|
|
60
|
+
proxy_socket.sendall(f"Host: {target_host}:{target_port}\r\n".encode())
|
|
61
|
+
|
|
62
|
+
if parsed_proxy_uri.auth is not None:
|
|
63
|
+
basic_auth_payload = base64.b64encode(parsed_proxy_uri.auth.encode()).decode()
|
|
64
|
+
proxy_socket.sendall(f"Proxy-Authorization: Basic {basic_auth_payload}\r\n".encode())
|
|
65
|
+
|
|
66
|
+
proxy_socket.sendall(b"\r\n")
|
|
67
|
+
response_line = proxy_socket.recv(4096).decode().split("\r\n")[0]
|
|
68
|
+
|
|
69
|
+
match = STATUS_LINE_PATTERN.match(response_line)
|
|
70
|
+
|
|
71
|
+
if match is None:
|
|
72
|
+
raise ValueError("status line pattern did not match")
|
|
73
|
+
|
|
74
|
+
response_code = int(match.group(1))
|
|
75
|
+
|
|
76
|
+
if response_code == 200:
|
|
77
|
+
return proxy_socket
|
|
78
|
+
|
|
79
|
+
last_response_code = response_code
|
|
80
|
+
if response_code in RETRYABLE_RESPONSE_CODES:
|
|
81
|
+
log.info("Received retryable response code from proxy, retrying after 0.5 seconds: %s", response_code)
|
|
82
|
+
time.sleep(0.5)
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise RuntimeError("Failed to create socket") from e
|
|
87
|
+
|
|
88
|
+
raise ValueError(f"Failed to establish tunnel, invalid response code: {last_response_code}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _create_ssl_socket(proxy_host: str, proxy_port: int) -> socket.socket:
|
|
92
|
+
ca_bundle_path = os.environ.get("REQUESTS_CA_BUNDLE")
|
|
93
|
+
if not ca_bundle_path or not os.path.isfile(ca_bundle_path):
|
|
94
|
+
log.warning("The REQUESTS_CA_BUNDLE environment variable does not exist or is not a file.")
|
|
95
|
+
raise ValueError("REQUESTS_CA_BUNDLE does not exist")
|
|
96
|
+
if not os.access(ca_bundle_path, os.R_OK):
|
|
97
|
+
log.warning("The REQUESTS_CA_BUNDLE file is not readable.")
|
|
98
|
+
raise ValueError("REQUESTS_CA_BUNDLE is not readable")
|
|
99
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
100
|
+
context.load_verify_locations(ca_bundle_path)
|
|
101
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
102
|
+
wrapped_sock = context.wrap_socket(sock, server_hostname=proxy_host)
|
|
103
|
+
wrapped_sock.connect((proxy_host, proxy_port))
|
|
104
|
+
return wrapped_sock
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import random
|
|
18
|
+
import socket
|
|
19
|
+
from functools import cached_property
|
|
20
|
+
from tempfile import NamedTemporaryFile
|
|
21
|
+
from typing import Any, Mapping, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
import urllib3.util
|
|
24
|
+
from frozendict import frozendict
|
|
25
|
+
from requests import Session
|
|
26
|
+
|
|
27
|
+
from ._api import AwsCredentials, ClientCertificateFilePaths, SourceCredentials, SourceParameters
|
|
28
|
+
from ._connections import HttpsConnection
|
|
29
|
+
from ._proxies import create_proxy_session
|
|
30
|
+
from ._refreshable import DefaultSessionCredentialsManager, Refreshable, RefreshHandler
|
|
31
|
+
from ._sockets import create_socket
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Source:
|
|
37
|
+
"""
|
|
38
|
+
A class representing a Source for an external systems.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
source_parameters: SourceParameters,
|
|
44
|
+
on_prem_proxy_service_uris: list[str],
|
|
45
|
+
egress_proxy_service_uris: list[str],
|
|
46
|
+
egress_proxy_token: Optional[str],
|
|
47
|
+
source_configuration: Optional[Any],
|
|
48
|
+
credentials_refresh_handler: Optional[RefreshHandler[SourceCredentials]] = None,
|
|
49
|
+
):
|
|
50
|
+
self._source_parameters = source_parameters
|
|
51
|
+
self._on_prem_proxy_service_uris = on_prem_proxy_service_uris
|
|
52
|
+
self._egress_proxy_service_uris = egress_proxy_service_uris
|
|
53
|
+
self._source_configuration = source_configuration
|
|
54
|
+
self._egress_proxy_token = egress_proxy_token
|
|
55
|
+
self._credentials_refresh_handler = credentials_refresh_handler
|
|
56
|
+
|
|
57
|
+
@cached_property
|
|
58
|
+
def secrets(self) -> Mapping[str, str]:
|
|
59
|
+
"""A dictionary containing plaintext secrets on the Source keyed by secret API name."""
|
|
60
|
+
return frozendict(self._source_parameters.secrets)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def client_certificate(self) -> Optional[ClientCertificateFilePaths]:
|
|
64
|
+
"""
|
|
65
|
+
The client certificate file name and private key file name if present on the Source.
|
|
66
|
+
Returns:
|
|
67
|
+
Optional[ClientCertificateFilePaths]: If a client certificate is present on the source, otherwise None.
|
|
68
|
+
"""
|
|
69
|
+
return ClientCertificateFilePaths(*self._client_certificate) if self._client_certificate is not None else None
|
|
70
|
+
|
|
71
|
+
@cached_property
|
|
72
|
+
def _https_connections(self) -> Mapping[str, HttpsConnection]:
|
|
73
|
+
return frozendict(
|
|
74
|
+
{
|
|
75
|
+
key: HttpsConnection(params, self._client_certificate, self._https_proxy_url, self._ca_bundle_path)
|
|
76
|
+
for key, params in self._source_parameters.https_connections.items()
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@cached_property
|
|
81
|
+
def _ca_bundle_path(self) -> Optional[str]:
|
|
82
|
+
if self._source_parameters.server_certificates is None:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
new_ca_contents = []
|
|
86
|
+
|
|
87
|
+
# If requests env var is set, add all existing CAs
|
|
88
|
+
requests_ca_bundle_path = os.environ.get("REQUESTS_CA_BUNDLE")
|
|
89
|
+
if requests_ca_bundle_path is not None:
|
|
90
|
+
with open(requests_ca_bundle_path) as requests_ca_bundle_file:
|
|
91
|
+
new_ca_contents.append(requests_ca_bundle_file.read())
|
|
92
|
+
|
|
93
|
+
# Add all CAs for the source
|
|
94
|
+
for required_ca in self._source_parameters.server_certificates.values():
|
|
95
|
+
new_ca_contents.append(required_ca)
|
|
96
|
+
|
|
97
|
+
with NamedTemporaryFile(delete=False, mode="w") as ca_bundle_file:
|
|
98
|
+
ca_bundle_file.write(os.linesep.join(new_ca_contents) + os.linesep)
|
|
99
|
+
return ca_bundle_file.name
|
|
100
|
+
|
|
101
|
+
@cached_property
|
|
102
|
+
def _client_certificate(self) -> Optional[Tuple[str, str]]:
|
|
103
|
+
if self._source_parameters.client_certificate is None:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
cert_file = NamedTemporaryFile(delete=False, mode="w") # pylint: disable=consider-using-with
|
|
107
|
+
cert_file.write(self._source_parameters.client_certificate.pem_certificate)
|
|
108
|
+
|
|
109
|
+
private_key_file = NamedTemporaryFile(delete=False, mode="w") # pylint: disable=consider-using-with
|
|
110
|
+
private_key_file.write(self._source_parameters.client_certificate.pem_private_key)
|
|
111
|
+
|
|
112
|
+
return cert_file.name, private_key_file.name
|
|
113
|
+
|
|
114
|
+
@cached_property
|
|
115
|
+
def _proxy_session(self) -> Session:
|
|
116
|
+
egress_proxy_configured = self._egress_proxy_token is not None
|
|
117
|
+
on_prem_proxy_configured = self._source_parameters.proxy_token is not None
|
|
118
|
+
|
|
119
|
+
if on_prem_proxy_configured:
|
|
120
|
+
if egress_proxy_configured:
|
|
121
|
+
log.warning(
|
|
122
|
+
"both egress proxy and on-prem proxy are configured for this source. preferring on-prem proxy"
|
|
123
|
+
)
|
|
124
|
+
if len(self._on_prem_proxy_service_uris) == 0:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"on-prem proxy was configured for this source, but on-prem proxy URIs were not present"
|
|
127
|
+
)
|
|
128
|
+
return create_proxy_session(
|
|
129
|
+
random.choice(self._on_prem_proxy_service_uris), self._source_parameters.proxy_token
|
|
130
|
+
)
|
|
131
|
+
elif egress_proxy_configured:
|
|
132
|
+
# we checked this earlier, but assert again here to make mypy happy
|
|
133
|
+
assert (
|
|
134
|
+
self._egress_proxy_token is not None
|
|
135
|
+
), "no egress proxy parameters found while configuring egress proxy session"
|
|
136
|
+
if len(self._egress_proxy_service_uris) == 0:
|
|
137
|
+
raise ValueError("egress proxy was configured for this source, but egress proxy URIs were not present")
|
|
138
|
+
return create_proxy_session(self._https_proxy_url, self._egress_proxy_token)
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"neither egress proxy nor on-prem proxy were configured. unable to construct a proxy session"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@cached_property
|
|
145
|
+
def _https_proxy_url(self) -> Optional[str]:
|
|
146
|
+
if self._source_parameters.proxy_token is not None:
|
|
147
|
+
parsed = urllib3.util.parse_url(random.choice(self._on_prem_proxy_service_uris))
|
|
148
|
+
parsed = parsed._replace(auth=f"user:{self._source_parameters.proxy_token}")
|
|
149
|
+
return parsed.url
|
|
150
|
+
|
|
151
|
+
if self._egress_proxy_token is not None:
|
|
152
|
+
parsed = urllib3.util.parse_url(random.choice(self._egress_proxy_service_uris))
|
|
153
|
+
parsed = parsed._replace(auth=f"user:{self._egress_proxy_token}")
|
|
154
|
+
return parsed.url
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
@cached_property
|
|
159
|
+
def _maybe_refreshable_resolved_source_credentials(self) -> Optional[Refreshable[SourceCredentials]]:
|
|
160
|
+
if self._source_parameters.resolved_source_credentials is None:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
return DefaultSessionCredentialsManager(
|
|
164
|
+
self._source_parameters.resolved_source_credentials, self._credentials_refresh_handler
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@cached_property
|
|
168
|
+
def source_configuration(self) -> Any:
|
|
169
|
+
"""
|
|
170
|
+
Full configuration of the Source.
|
|
171
|
+
Throws if source configuration is not supported by public API.
|
|
172
|
+
"""
|
|
173
|
+
if self._source_configuration is None:
|
|
174
|
+
raise ValueError("Source configuration is not available for this Source.")
|
|
175
|
+
return self._source_configuration
|
|
176
|
+
|
|
177
|
+
def get_aws_credentials(self) -> Refreshable[AwsCredentials]:
|
|
178
|
+
"""
|
|
179
|
+
Get the AWS credentials from the Source.
|
|
180
|
+
|
|
181
|
+
Supported Sources:
|
|
182
|
+
- S3
|
|
183
|
+
"""
|
|
184
|
+
if self._maybe_refreshable_resolved_source_credentials is None:
|
|
185
|
+
raise ValueError("Resolved source credentials are not present on the Source.")
|
|
186
|
+
if not isinstance(self._maybe_refreshable_resolved_source_credentials.get(), AwsCredentials):
|
|
187
|
+
raise ValueError("Resolved source credentials are not of type AwsCredentials.")
|
|
188
|
+
return self._maybe_refreshable_resolved_source_credentials
|
|
189
|
+
|
|
190
|
+
def get_secret(self, key: str) -> str:
|
|
191
|
+
"""Get the plaintext value for the provided secret key, as configured in the Source secrets."""
|
|
192
|
+
secret = self.secrets.get(key)
|
|
193
|
+
if not secret:
|
|
194
|
+
raise ValueError(f"Secret with name {key} not found on the Source.")
|
|
195
|
+
return secret
|
|
196
|
+
|
|
197
|
+
def get_https_connection(self) -> HttpsConnection:
|
|
198
|
+
"""Get the HTTPS Connection from the Source.
|
|
199
|
+
Raises:
|
|
200
|
+
ValueError: If there are multiple connections on the source.
|
|
201
|
+
Returns:
|
|
202
|
+
HttpsConnection: The requested HttpsConnection object.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
if len(self._https_connections) != 1:
|
|
206
|
+
raise ValueError("Only single connection sources are supported.")
|
|
207
|
+
return next(iter(self._https_connections.values()))
|
|
208
|
+
|
|
209
|
+
def get_https_proxy_uri(self) -> Optional[str]:
|
|
210
|
+
"""Get the HTTPS proxy URI that must be used to communicate with the external system
|
|
211
|
+
when using an Agent Proxy source.
|
|
212
|
+
The format of the proxy URI will be "https://user:password@proxy-server.com:443".
|
|
213
|
+
If a proxy is not required, the function will return None.
|
|
214
|
+
This is useful if you can't use the provided requests client and need to use a different HTTP client or an SDK.
|
|
215
|
+
"""
|
|
216
|
+
return self._https_proxy_url
|
|
217
|
+
|
|
218
|
+
def create_socket(self, target_host: str, target_port: int) -> socket.socket: # pylint: disable=too-many-locals
|
|
219
|
+
"""
|
|
220
|
+
Create a socket connection to the host and port in the HTTPS connection through the Agent Proxy.
|
|
221
|
+
The socket returned by this method MUST be closed by the caller to avoid hanging connections in the proxy.
|
|
222
|
+
It is recommended to use the `contextlib.closing` context manager to ensure the socket is closed:
|
|
223
|
+
from contextlib import closing
|
|
224
|
+
with closing(source.create_socket(target_host, target_port)) as sock:
|
|
225
|
+
# Use the socket here
|
|
226
|
+
Returns:
|
|
227
|
+
socket.socket: A socket object connected to the target host and port through the HTTPS proxy.
|
|
228
|
+
Raises:
|
|
229
|
+
ValueError: If the configured proxy is not an HTTPS proxy or if the tunnel cannot be established.
|
|
230
|
+
RuntimeError: If there is a failure during socket creation.
|
|
231
|
+
"""
|
|
232
|
+
if not self._https_proxy_url:
|
|
233
|
+
raise ValueError("Only usable with Agent Proxy Sources")
|
|
234
|
+
|
|
235
|
+
return create_socket(self._https_proxy_url, target_host, target_port)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2025 Palantir Technologies, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from ._api import SourceCredentials
|
|
16
|
+
|
|
17
|
+
JAVA_OFFSET_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def has_expiration_property(source_credentials: SourceCredentials) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Checks if the source credentials provided have an expiration property.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
return hasattr(source_credentials, "expiration")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright {YEAR} Palantir Technologies, Inc.
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: external-systems
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for interacting with Foundry Sources
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: Palantir,Foundry,Sources,Compute Modules,Python Functions,Transforms
|
|
7
|
+
Author: Palantir Technologies, Inc.
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: frozendict (>=2.4.6,<3.0.0)
|
|
17
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
18
|
+
Project-URL: Repository, https://github.com/palantir/external-systems
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# External Systems
|
|
22
|
+

|
|
23
|
+
[](https://pypi.org/project/external-systems/)
|
|
24
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
> [!WARNING]
|
|
28
|
+
> This SDK is incubating and subject to change.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## About Foundry Sources
|
|
32
|
+
|
|
33
|
+
The External Systems library is Python SDK built as an interface to reference [Foundry Sources](https://www.palantir.com/docs/foundry/data-connection/set-up-source) from code.
|
|
34
|
+
|
|
35
|
+
<a id="installation"></a>
|
|
36
|
+
## Installation
|
|
37
|
+
You can install the Python package using `pip`:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
pip install external-systems
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
<a id="basic-usage"></a>
|
|
44
|
+
## Basic Source Usage
|
|
45
|
+
|
|
46
|
+
### Credentials
|
|
47
|
+
|
|
48
|
+
Long lived credentials can be referenced using `get_secret()` on the source.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
my_source: Source = ...
|
|
52
|
+
|
|
53
|
+
some_secret = my_source.get_secret("SECRET_NAME")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For sources using session credentials we support credentials generation and refresh management. Currently on an S3 source you can access session credentials using `get_aws_credentials()`.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
s3_source: Source = ...
|
|
60
|
+
|
|
61
|
+
refreshable_credentials: Refreshable[AwsCredentials] = s3_source.get_aws_credentials()
|
|
62
|
+
|
|
63
|
+
session_credentials: AwsCredentials = refreshable_credentials.get()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### HTTP Client
|
|
67
|
+
For REST based sources, a preconfigured HTTP client is provided built on top of the Python requests library.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
source_url = my_source.get_https_connection().url
|
|
71
|
+
http_client = my_source.get_https_connection().get_client()
|
|
72
|
+
|
|
73
|
+
response = http_client.get(source_url + "/api/v1/example/", timeout=10)
|
|
74
|
+
```
|
|
75
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
external_systems/__init__.py,sha256=xXDUDD6_qRO-nHuXZx-fXp0R0vc3N_OOsB1F5mF_kpU,651
|
|
2
|
+
external_systems/_version.py,sha256=edsEuFM8fnpvqgnm1a9kDAaQcgfl58B_iUMYwpkVdRw,747
|
|
3
|
+
external_systems/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
external_systems/sources/__init__.py,sha256=aqXbMIy_pnyIC1uPRzQfApLCbhYB4N8iRFnpOX4RdAk,1156
|
|
5
|
+
external_systems/sources/_api.py,sha256=NV7oNIgzSWz4ROFW8uPpUJjDN5vfFzdTs3yKC37S39k,3262
|
|
6
|
+
external_systems/sources/_connections.py,sha256=h82npEks29NALAAfMHrXcSnB7TY_OLeXgL3QN9Cssus,2509
|
|
7
|
+
external_systems/sources/_proxies.py,sha256=igWRL-kpQ_IlZyJwI_s66rDe0oQ68ltGeFNydgoiR4w,5071
|
|
8
|
+
external_systems/sources/_refreshable.py,sha256=0pa5XW0_2gTMW-ZymKhlhh3Kka_bWTWffThKvrTTifc,4421
|
|
9
|
+
external_systems/sources/_sockets.py,sha256=oCCUEpLvHLhYXljRiBc11YAWJCZOd-wpUFjl8CaueX0,4285
|
|
10
|
+
external_systems/sources/_sources.py,sha256=fYEkVOVCuEHpJytRWw-TCXzHmefoAhAny5853fONajU,10529
|
|
11
|
+
external_systems/sources/_utils.py,sha256=_GDyJOzPHQudEbrPsRhXMs8u6AaHpgJQIq8H9xSYJZk,913
|
|
12
|
+
external_systems-0.1.0.dist-info/LICENSE.txt,sha256=NAk6Uc9K_N_J5V75k9qECpzUnO-ujT-mKK_jk_mboUE,569
|
|
13
|
+
external_systems-0.1.0.dist-info/METADATA,sha256=isfctYmHoIoGFyNpoFu1W0GqOyW-mTK79UG1nvsVVMI,2513
|
|
14
|
+
external_systems-0.1.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
15
|
+
external_systems-0.1.0.dist-info/RECORD,,
|