truefoundry 0.2.10__py3-none-any.whl → 0.3.0rc1__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 truefoundry might be problematic. Click here for more details.

Files changed (99) hide show
  1. truefoundry/__init__.py +1 -0
  2. truefoundry/autodeploy/cli.py +31 -18
  3. truefoundry/deploy/__init__.py +118 -1
  4. truefoundry/deploy/auto_gen/models.py +1675 -0
  5. truefoundry/deploy/builder/__init__.py +116 -0
  6. truefoundry/deploy/builder/builders/__init__.py +22 -0
  7. truefoundry/deploy/builder/builders/dockerfile.py +57 -0
  8. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/__init__.py +44 -0
  9. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +51 -0
  10. truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py +44 -0
  11. truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py +158 -0
  12. truefoundry/deploy/builder/docker_service.py +168 -0
  13. truefoundry/deploy/cli/cli.py +19 -26
  14. truefoundry/deploy/cli/commands/__init__.py +18 -0
  15. truefoundry/deploy/cli/commands/apply_command.py +52 -0
  16. truefoundry/deploy/cli/commands/build_command.py +45 -0
  17. truefoundry/deploy/cli/commands/build_logs_command.py +89 -0
  18. truefoundry/deploy/cli/commands/create_command.py +75 -0
  19. truefoundry/deploy/cli/commands/delete_command.py +77 -0
  20. truefoundry/deploy/cli/commands/deploy_command.py +99 -0
  21. truefoundry/deploy/cli/commands/get_command.py +216 -0
  22. truefoundry/deploy/cli/commands/list_command.py +171 -0
  23. truefoundry/deploy/cli/commands/login_command.py +33 -0
  24. truefoundry/deploy/cli/commands/logout_command.py +20 -0
  25. truefoundry/deploy/cli/commands/logs_command.py +134 -0
  26. truefoundry/deploy/cli/commands/patch_application_command.py +79 -0
  27. truefoundry/deploy/cli/commands/patch_command.py +70 -0
  28. truefoundry/deploy/cli/commands/redeploy_command.py +41 -0
  29. truefoundry/deploy/cli/commands/terminate_comand.py +44 -0
  30. truefoundry/deploy/cli/commands/trigger_command.py +87 -0
  31. truefoundry/deploy/cli/config.py +10 -0
  32. truefoundry/deploy/cli/console.py +5 -0
  33. truefoundry/deploy/cli/const.py +12 -0
  34. truefoundry/deploy/cli/display_util.py +118 -0
  35. truefoundry/deploy/cli/util.py +92 -0
  36. truefoundry/deploy/core/__init__.py +7 -0
  37. truefoundry/deploy/core/login.py +9 -0
  38. truefoundry/deploy/core/logout.py +5 -0
  39. truefoundry/deploy/function_service/__init__.py +3 -0
  40. truefoundry/deploy/function_service/__main__.py +27 -0
  41. truefoundry/deploy/function_service/app.py +92 -0
  42. truefoundry/deploy/function_service/build.py +45 -0
  43. truefoundry/deploy/function_service/remote/__init__.py +6 -0
  44. truefoundry/deploy/function_service/remote/context.py +3 -0
  45. truefoundry/deploy/function_service/remote/method.py +67 -0
  46. truefoundry/deploy/function_service/remote/remote.py +144 -0
  47. truefoundry/deploy/function_service/route.py +137 -0
  48. truefoundry/deploy/function_service/service.py +113 -0
  49. truefoundry/deploy/function_service/utils.py +53 -0
  50. truefoundry/deploy/io/__init__.py +0 -0
  51. truefoundry/deploy/io/output_callback.py +23 -0
  52. truefoundry/deploy/io/rich_output_callback.py +27 -0
  53. truefoundry/deploy/json_util.py +7 -0
  54. truefoundry/deploy/lib/__init__.py +0 -0
  55. truefoundry/deploy/lib/auth/auth_service_client.py +81 -0
  56. truefoundry/deploy/lib/auth/credential_file_manager.py +115 -0
  57. truefoundry/deploy/lib/auth/credential_provider.py +131 -0
  58. truefoundry/deploy/lib/auth/servicefoundry_session.py +59 -0
  59. truefoundry/deploy/lib/clients/__init__.py +0 -0
  60. truefoundry/deploy/lib/clients/servicefoundry_client.py +723 -0
  61. truefoundry/deploy/lib/clients/shell_client.py +13 -0
  62. truefoundry/deploy/lib/clients/utils.py +41 -0
  63. truefoundry/deploy/lib/const.py +43 -0
  64. truefoundry/deploy/lib/dao/__init__.py +0 -0
  65. truefoundry/deploy/lib/dao/application.py +246 -0
  66. truefoundry/deploy/lib/dao/apply.py +80 -0
  67. truefoundry/deploy/lib/dao/version.py +33 -0
  68. truefoundry/deploy/lib/dao/workspace.py +71 -0
  69. truefoundry/deploy/lib/exceptions.py +23 -0
  70. truefoundry/deploy/lib/logs_utils.py +43 -0
  71. truefoundry/deploy/lib/messages.py +12 -0
  72. truefoundry/deploy/lib/model/__init__.py +0 -0
  73. truefoundry/deploy/lib/model/entity.py +382 -0
  74. truefoundry/deploy/lib/session.py +146 -0
  75. truefoundry/deploy/lib/util.py +70 -0
  76. truefoundry/deploy/lib/win32.py +129 -0
  77. truefoundry/deploy/v2/__init__.py +0 -0
  78. truefoundry/deploy/v2/lib/__init__.py +3 -0
  79. truefoundry/deploy/v2/lib/deploy.py +232 -0
  80. truefoundry/deploy/v2/lib/deployable_patched_models.py +68 -0
  81. truefoundry/deploy/v2/lib/models.py +53 -0
  82. truefoundry/deploy/v2/lib/patched_models.py +497 -0
  83. truefoundry/deploy/v2/lib/source.py +267 -0
  84. truefoundry/langchain/__init__.py +12 -1
  85. truefoundry/langchain/deprecated.py +302 -0
  86. truefoundry/langchain/truefoundry_chat.py +130 -0
  87. truefoundry/langchain/truefoundry_embeddings.py +171 -0
  88. truefoundry/langchain/truefoundry_llm.py +106 -0
  89. truefoundry/langchain/utils.py +85 -0
  90. truefoundry/logger.py +17 -0
  91. truefoundry/pydantic_v1.py +5 -0
  92. truefoundry/python_deploy_codegen.py +132 -0
  93. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/METADATA +22 -5
  94. truefoundry-0.3.0rc1.dist-info/RECORD +124 -0
  95. truefoundry/deploy/cli/deploy.py +0 -165
  96. truefoundry-0.2.10.dist-info/RECORD +0 -38
  97. /truefoundry/{deploy/cli/version.py → version.py} +0 -0
  98. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/WHEEL +0 -0
  99. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,3 @@
1
+ from typing import Dict
2
+
3
+ QUAL_NAME_TO_URL_MAP: Dict[str, str] = {}
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import json
5
+ from typing import TYPE_CHECKING
6
+
7
+ import requests
8
+
9
+ from truefoundry.deploy.function_service.remote.context import QUAL_NAME_TO_URL_MAP
10
+
11
+ if TYPE_CHECKING:
12
+ from truefoundry.deploy.function_service.remote.remote import Remote
13
+
14
+
15
+ class Method(abc.ABC):
16
+ def __call__(self, *args, **kwargs):
17
+ if len(args) > 0:
18
+ raise Exception(
19
+ "Positional arguments are not supported now.\n"
20
+ "Please use keyword arguments"
21
+ )
22
+ return self.run(**kwargs)
23
+
24
+ @abc.abstractmethod
25
+ def run(self, **kwargs): ...
26
+
27
+ @abc.abstractmethod
28
+ async def run_async(self, **kwargs): ...
29
+
30
+
31
+ class LocalMethod(Method):
32
+ def __init__(self, remote_object: Remote, method):
33
+ self._remote_object = remote_object
34
+ self._method_name = method.__name__
35
+
36
+ def run(self, **kwargs):
37
+ return getattr(self._remote_object.instance, self._method_name)(**kwargs)
38
+
39
+ async def run_async(self, **kwargs):
40
+ raise NotImplementedError()
41
+
42
+
43
+ class RemoteMethod(Method):
44
+ def __init__(self, remote_object: Remote, method):
45
+ self._remote_object = remote_object
46
+ self._method_name = method.__name__
47
+ self._qual_name = self._remote_object.get_qual_name(method)
48
+
49
+ def check(self) -> bool:
50
+ return self._qual_name in QUAL_NAME_TO_URL_MAP
51
+
52
+ def run(self, **kwargs):
53
+ url = QUAL_NAME_TO_URL_MAP.get(self._qual_name)
54
+ r = requests.post(url, json=kwargs)
55
+ assert r.status_code == 200, r.text
56
+ return json.loads(r.text)
57
+
58
+ async def run_async(self, **kwargs):
59
+ raise NotImplementedError()
60
+
61
+
62
+ def method_factory(*args, **kwargs) -> Method:
63
+ remote_method = RemoteMethod(*args, **kwargs)
64
+ if remote_method.check():
65
+ return remote_method
66
+
67
+ return LocalMethod(*args, **kwargs)
@@ -0,0 +1,144 @@
1
+ import abc
2
+ import inspect
3
+ from typing import Dict, Optional
4
+
5
+ from truefoundry.deploy.function_service.remote.method import method_factory
6
+ from truefoundry.deploy.function_service.utils import get_qual_name
7
+
8
+
9
+ class Remote(abc.ABC):
10
+ @property
11
+ @abc.abstractmethod
12
+ def instance(self): ...
13
+
14
+ @abc.abstractmethod
15
+ def get_qual_name(self, method) -> str: ...
16
+
17
+
18
+ class RemoteFunction(Remote):
19
+ def __init__(self, func, **kwargs):
20
+ self._func = func
21
+ self._instance = inspect.getmodule(self._func)
22
+ self.method = method_factory(remote_object=self, method=self._func)
23
+
24
+ def __call__(self, *args, **kwargs):
25
+ if len(args) > 0:
26
+ raise Exception(
27
+ "Positional arguments are not supported now.\n"
28
+ "Please use keyword arguments"
29
+ )
30
+ return self.method.__call__(**kwargs)
31
+
32
+ def run(self, **kwargs):
33
+ return self.method.run(**kwargs)
34
+
35
+ async def run_async(self, **kwargs):
36
+ return await self.method.run_async(**kwargs)
37
+
38
+ @property
39
+ def instance(self):
40
+ return self._instance
41
+
42
+ def get_qual_name(self, method) -> str:
43
+ return get_qual_name(method)
44
+
45
+
46
+ class RemoteClass(Remote):
47
+ def __init__(
48
+ self,
49
+ class_,
50
+ *,
51
+ init_kwargs: Optional[Dict] = None,
52
+ name: Optional[str] = None,
53
+ **kwargs,
54
+ ):
55
+ self._class = class_
56
+ self._name = name or class_.__name__
57
+ self._init_kwargs = init_kwargs or {}
58
+
59
+ self._instance = None
60
+
61
+ ###############
62
+ # Maybe this is not the right way to model it.
63
+ # Another approach can be, identify whether the environment is local
64
+ # or our service. If local, then we can just return the instance of
65
+ # the user defined class. Else, we return a class which reflects the
66
+ # methods in the class, but internally doing network call.
67
+ # But I am sticking with this now because not all the concepts of a python
68
+ # class right now, will work in both remote and local. Examples, you cannot
69
+ # access arbitary attributes, properties etc. I want to keep the behaviour
70
+ # identical for now.
71
+ absent = object()
72
+ methods = inspect.getmembers(class_, predicate=inspect.isfunction)
73
+ for method_name, method in methods:
74
+ if method_name.startswith("_"):
75
+ continue
76
+ existing_attribute = getattr(self, method_name, absent)
77
+ if existing_attribute is not absent:
78
+ raise Exception(
79
+ f"Cannot reflect {method_name!r} of class {class_.__name__!r}.\n"
80
+ f"{method_name!r} is reserved. Use a different method name"
81
+ )
82
+
83
+ setattr(
84
+ self,
85
+ method_name,
86
+ method_factory(remote_object=self, method=method),
87
+ )
88
+ ###############
89
+
90
+ def get_qual_name(self, method) -> str:
91
+ return f"{self.name}:{get_qual_name(method)}"
92
+
93
+ @property
94
+ def instance(self):
95
+ if self._instance:
96
+ return self._instance
97
+ instance = self._class(**self._init_kwargs)
98
+ self._instance = instance
99
+ return self._instance
100
+
101
+ @property
102
+ def class_(self):
103
+ return self._class
104
+
105
+ @property
106
+ def name(self) -> str:
107
+ return self._name
108
+
109
+ @property
110
+ def init_kwargs(self) -> Dict:
111
+ return self._init_kwargs
112
+
113
+
114
+ def remote(func_or_class, **kwargs):
115
+ if inspect.isfunction(func_or_class):
116
+ return RemoteFunction(func_or_class, **kwargs)
117
+ if inspect.isclass(func_or_class):
118
+ return RemoteClass(func_or_class, **kwargs)
119
+
120
+ raise Exception()
121
+
122
+
123
+ if __name__ == "__main__":
124
+
125
+ def foo(a, b):
126
+ print(a, b)
127
+
128
+ remote(foo)(a=1, b=2)
129
+
130
+ class Foo:
131
+ def __init__(self, a, b):
132
+ self.a = a
133
+ self.b = b
134
+
135
+ def foo(self):
136
+ print(self.a, self.b)
137
+
138
+ foo_instance = remote(Foo, name="foo", init_kwargs={"a": 1, "b": 2})
139
+ foo_instance_2 = remote(Foo, name="foo_2", init_kwargs={"a": 3, "b": 2})
140
+
141
+ foo_instance.foo()
142
+ foo_instance_2.foo()
143
+ foo_instance.foo.run()
144
+ # foo_instance.foo.async_run()
@@ -0,0 +1,137 @@
1
+ import inspect
2
+ import json
3
+ from typing import Any, Callable, Dict, List
4
+
5
+ from truefoundry.deploy.function_service.remote import RemoteClass
6
+ from truefoundry.deploy.function_service.utils import (
7
+ create_pydantic_model_from_function_signature,
8
+ get_qual_name,
9
+ )
10
+ from truefoundry.logger import logger
11
+ from truefoundry.pydantic_v1 import BaseModel, Field, constr, validator
12
+
13
+
14
+ def validate_we_can_create_pydantic_model_of_func_args(func: Callable):
15
+ qual_name = get_qual_name(func)
16
+ try:
17
+ create_pydantic_model_from_function_signature(func, get_qual_name(func))
18
+ except Exception as ex:
19
+ raise Exception(
20
+ f"Unable to create a route for {qual_name!r}.\n"
21
+ "Please ensure that in the function type signature, you have only used in-built\n"
22
+ "types like `int`, `float`, `str`, `bool`, `typing.Dict`, `typing.List`, typing.Optional`.\n"
23
+ "To temporarily resolve this error, you can remove the unsupported type signatures.\n"
24
+ ) from ex
25
+
26
+
27
+ def path_pre_processor(path: str, prefix: str = "") -> str:
28
+ path = path.strip("/")
29
+
30
+ if not path:
31
+ raise ValueError("path cannot be empty")
32
+
33
+ prefix = prefix.strip("/")
34
+ if not prefix:
35
+ return f"/{path}"
36
+
37
+ return f"/{prefix}/{path}"
38
+
39
+
40
+ class Route(BaseModel):
41
+ function_name: str
42
+ http_method: str
43
+ path: constr(regex=r"^[A-Za-z0-9\-_/]+$")
44
+
45
+ qual_name: str
46
+
47
+
48
+ class FunctionRoute(Route):
49
+ module: str
50
+
51
+ @classmethod
52
+ def from_func(cls, func: Callable, path: str):
53
+ validate_we_can_create_pydantic_model_of_func_args(func)
54
+ return cls(
55
+ function_name=func.__name__,
56
+ http_method="POST",
57
+ path=path_pre_processor(path or func.__name__),
58
+ qual_name=get_qual_name(func),
59
+ module=func.__module__,
60
+ )
61
+
62
+
63
+ class ClassRoute(BaseModel):
64
+ class_name: str
65
+ init_kwargs: Dict[str, Any] = Field(default_factory=dict)
66
+ module: str
67
+
68
+ routes: List[Route] = Field(default_factory=list)
69
+
70
+ @validator("init_kwargs")
71
+ def init_kwargs_is_json_serializable(cls, v, values):
72
+ try:
73
+ json.dumps(v)
74
+ except Exception as ex:
75
+ class_name = values.get("class_name")
76
+ raise ValueError(
77
+ f"init_kwargs {v!r} of class {class_name!r} is not JSON serializable"
78
+ ) from ex
79
+
80
+ return v
81
+
82
+ @classmethod
83
+ def from_class(cls, remote_class: RemoteClass):
84
+ routes = []
85
+ methods = inspect.getmembers(remote_class.class_, predicate=inspect.isfunction)
86
+
87
+ for method_name, method in methods:
88
+ if method_name.startswith("_"):
89
+ continue
90
+ validate_we_can_create_pydantic_model_of_func_args(method)
91
+ route = Route(
92
+ function_name=method_name,
93
+ http_method="POST",
94
+ path=path_pre_processor(prefix=remote_class.name, path=method_name),
95
+ qual_name=remote_class.get_qual_name(method),
96
+ )
97
+ routes.append(route)
98
+
99
+ return cls(
100
+ class_name=remote_class.class_.__name__,
101
+ init_kwargs=remote_class.init_kwargs,
102
+ routes=routes,
103
+ module=remote_class.class_.__module__,
104
+ )
105
+
106
+
107
+ class RouteGroups(BaseModel):
108
+ functions: List[FunctionRoute] = Field(default_factory=list)
109
+ classes: Dict[str, ClassRoute] = Field(default_factory=dict)
110
+
111
+ def register_function(self, func, path):
112
+ function_route = FunctionRoute.from_func(func=func, path=path)
113
+ logger.info(
114
+ "Function %r from module %r will be deployed on path '%s %s'.",
115
+ function_route.function_name,
116
+ function_route.module,
117
+ function_route.http_method,
118
+ function_route.path,
119
+ )
120
+ self.functions.append(function_route)
121
+
122
+ def register_class(self, remote_class: RemoteClass):
123
+ if remote_class.name in self.classes:
124
+ raise ValueError(
125
+ f"name {remote_class.name!r} is already used to register a class"
126
+ )
127
+ class_route = ClassRoute.from_class(remote_class)
128
+ for route in class_route.routes:
129
+ logger.info(
130
+ "Method %r from `%s:%s` will be deployed on path '%s %s'.",
131
+ route.function_name,
132
+ class_route.class_name,
133
+ remote_class.name,
134
+ route.http_method,
135
+ route.path,
136
+ )
137
+ self.classes[remote_class.name] = class_route
@@ -0,0 +1,113 @@
1
+ from threading import Thread
2
+ from typing import Any, Callable, Dict, Optional, Union
3
+
4
+ import yaml
5
+
6
+ from truefoundry.deploy.auto_gen.models import Port, Resources
7
+ from truefoundry.deploy.function_service.app import (
8
+ build_and_run_app_in_background_thread,
9
+ )
10
+ from truefoundry.deploy.function_service.build import BuildConfig
11
+ from truefoundry.deploy.function_service.remote import RemoteClass
12
+ from truefoundry.deploy.function_service.route import RouteGroups
13
+ from truefoundry.deploy.v2.lib.deployable_patched_models import Service
14
+ from truefoundry.deploy.v2.lib.patched_models import Build, LocalSource
15
+ from truefoundry.logger import logger
16
+
17
+
18
+ class FunctionService:
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ build_config: Optional[BuildConfig] = None,
23
+ resources: Optional[Resources] = None,
24
+ replicas: int = 1,
25
+ port: Union[int, Port] = 8000,
26
+ env: Optional[Dict[str, str]] = None,
27
+ ):
28
+ self._name = name
29
+ self._build_config = build_config or BuildConfig()
30
+ self._resources = resources or Resources()
31
+ self._replicas = replicas
32
+ if isinstance(port, int):
33
+ port = Port(port=port, expose=False)
34
+ if not port.host:
35
+ logger.warning(
36
+ "No host is set for the port. This is not an issue if you don't "
37
+ "want an exposed endpoint or are just testing locally.\n"
38
+ "However, for actual deployment it is required to pass an "
39
+ "instance of `truefoundry.deploy.Port` with "
40
+ "`host` argument defined.\n"
41
+ "E.g. `FunctionService(name='...', port=Port(port=8000, host='...', path='...'), ...)`"
42
+ )
43
+ self._port = port
44
+ self._env = env or {}
45
+
46
+ self._route_groups: RouteGroups = RouteGroups()
47
+
48
+ @property
49
+ def route_groups(self) -> RouteGroups:
50
+ return self._route_groups
51
+
52
+ def __repr__(self):
53
+ return yaml.dump(
54
+ {
55
+ "name": self._name,
56
+ "build_config": self._build_config.dict(),
57
+ "resources": self._resources.dict(),
58
+ "routes": self._route_groups.dict(),
59
+ "replicas": self._replicas,
60
+ "port": self._port.dict(),
61
+ "env": self._env,
62
+ },
63
+ indent=2,
64
+ )
65
+
66
+ def register_function(
67
+ self,
68
+ func: Callable,
69
+ *,
70
+ path: Optional[str] = None,
71
+ ):
72
+ self._route_groups.register_function(func=func, path=path)
73
+
74
+ def register_class(
75
+ self,
76
+ class_,
77
+ *,
78
+ init_kwargs: Optional[Dict[str, Any]] = None,
79
+ name: Optional[str] = None,
80
+ ):
81
+ # TODO: I need to rethink this `RemoteClass`.
82
+ # I am mixing up multiple responsibilities here.
83
+ # For now, I am removing the burden of using `remote` from the user when deploying
84
+ # an instance of a class.
85
+ remote_class = RemoteClass(class_, init_kwargs=init_kwargs, name=name)
86
+ self._route_groups.register_class(remote_class=remote_class)
87
+
88
+ def run(self) -> Thread:
89
+ return build_and_run_app_in_background_thread(
90
+ route_groups=self._route_groups, port=self._port.port
91
+ )
92
+
93
+ def get_deployment_definition(self) -> Service:
94
+ # Keeping this function right now so that later,
95
+ # the constructor of the application call this function
96
+ # to get the component spec, if an object of this class
97
+ # is directly passed as a component
98
+ tfy_python_build_config = self._build_config.to_tfy_python_build_config(
99
+ port=self._port.port, route_groups=self._route_groups
100
+ )
101
+ service = Service(
102
+ name=self._name,
103
+ image=Build(build_source=LocalSource(), build_spec=tfy_python_build_config),
104
+ resources=self._resources,
105
+ replicas=self._replicas,
106
+ ports=[self._port],
107
+ env=self._env,
108
+ )
109
+ return service
110
+
111
+ def deploy(self, workspace_fqn: str, wait: bool = True):
112
+ service = self.get_deployment_definition()
113
+ service.deploy(workspace_fqn=workspace_fqn, wait=wait)
@@ -0,0 +1,53 @@
1
+ import inspect
2
+ from typing import Any
3
+
4
+ from truefoundry.pydantic_v1 import BaseModel
5
+ from truefoundry.pydantic_v1 import create_model as pydantic_create_model
6
+
7
+
8
+ def get_qual_name(obj):
9
+ return f"{obj.__module__}.{obj.__qualname__}"
10
+
11
+
12
+ def create_pydantic_model_from_function_signature(func, model_name: str):
13
+ # https://github.com/pydantic/pydantic/issues/1391
14
+ (
15
+ args,
16
+ _,
17
+ varkw,
18
+ defaults,
19
+ kwonlyargs,
20
+ kwonlydefaults,
21
+ annotations,
22
+ ) = inspect.getfullargspec(func)
23
+ defaults = defaults or []
24
+ args = args or []
25
+ if len(args) > 0 and args[0] == "self":
26
+ del args[0]
27
+
28
+ non_default_args = len(args) - len(defaults)
29
+ defaults = [
30
+ ...,
31
+ ] * non_default_args + list(defaults)
32
+
33
+ keyword_only_params = {
34
+ param: kwonlydefaults.get(param, Any) for param in kwonlyargs
35
+ }
36
+ params = {
37
+ param: (annotations.get(param, Any), default)
38
+ for param, default in zip(args, defaults)
39
+ }
40
+
41
+ class Config:
42
+ extra = "allow"
43
+
44
+ # Allow extra params if there is a **kwargs parameter in the function signature
45
+ config = Config if varkw else None
46
+
47
+ return pydantic_create_model(
48
+ model_name,
49
+ **params,
50
+ **keyword_only_params,
51
+ __base__=BaseModel,
52
+ __config__=config,
53
+ )
File without changes
@@ -0,0 +1,23 @@
1
+ class OutputCallBack:
2
+ def print_header(self, line):
3
+ print(line)
4
+
5
+ def _print_separator(self):
6
+ print("-" * 80)
7
+
8
+ def print_line(self, line):
9
+ print(line)
10
+
11
+ def print_lines_in_panel(self, lines, header=None):
12
+ self.print_header(header)
13
+ self._print_separator()
14
+ for line in lines:
15
+ self.print_line(line)
16
+ self._print_separator()
17
+
18
+ def print_code_in_panel(self, lines, header=None, lang="python"):
19
+ self.print_lines_in_panel(lines, header)
20
+
21
+ def print(self, line):
22
+ # just an alias
23
+ self.print_line(line)
@@ -0,0 +1,27 @@
1
+ from rich.console import Console
2
+ from rich.highlighter import ReprHighlighter
3
+ from rich.panel import Panel
4
+ from rich.text import Text
5
+
6
+ from truefoundry.deploy.io.output_callback import OutputCallBack
7
+
8
+
9
+ def _text(line):
10
+ return Text.from_ansi(str(line)) if "\x1b" in line else str(line)
11
+
12
+
13
+ class RichOutputCallBack(OutputCallBack):
14
+ console = Console(soft_wrap=True)
15
+ highlighter = ReprHighlighter()
16
+
17
+ def print_header(self, line):
18
+ self.console.rule(_text(line), style="cyan")
19
+
20
+ def print_line(self, line):
21
+ self.console.print(_text(line))
22
+
23
+ def print_lines_in_panel(self, lines, header=None):
24
+ self.console.print(Panel(self.highlighter("\n".join(lines)), title=header))
25
+
26
+ def print_code_in_panel(self, lines, header=None, lang="python"):
27
+ self.print_lines_in_panel(lines, header)
@@ -0,0 +1,7 @@
1
+ import datetime
2
+
3
+
4
+ def json_default_encoder(o):
5
+ if isinstance(o, datetime.datetime):
6
+ return o.isoformat()
7
+ raise TypeError(f"Cannot json encode {type(o)}: {o}")
File without changes
@@ -0,0 +1,81 @@
1
+ import time
2
+
3
+ import requests
4
+
5
+ from truefoundry.deploy.lib.clients.utils import poll_for_function, request_handling
6
+ from truefoundry.deploy.lib.const import VERSION_PREFIX
7
+ from truefoundry.deploy.lib.exceptions import BadRequestException
8
+ from truefoundry.deploy.lib.model.entity import DeviceCode, Token
9
+ from truefoundry.logger import logger
10
+
11
+
12
+ class AuthServiceClient:
13
+ def __init__(self, base_url):
14
+ from truefoundry.deploy.lib.clients.servicefoundry_client import (
15
+ ServiceFoundryServiceClient,
16
+ )
17
+
18
+ client = ServiceFoundryServiceClient(init_session=False, base_url=base_url)
19
+ tenant_info = client.get_tenant_info()
20
+
21
+ self._auth_server_url = tenant_info.auth_server_url
22
+ self._tenant_name = tenant_info.tenant_name
23
+
24
+ def refresh_token(self, token: Token, host: str = None) -> Token:
25
+ host_arg_str = f"--host {host}" if host else "--host HOST"
26
+ if not token.refresh_token:
27
+ # TODO: Add a way to propagate error messages without traceback to the output interface side
28
+ raise Exception(
29
+ f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
30
+ )
31
+ url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/token/refresh"
32
+ data = {
33
+ "tenantName": token.tenant_name,
34
+ "refreshToken": token.refresh_token,
35
+ }
36
+ res = requests.post(url, data=data)
37
+ try:
38
+ res = request_handling(res)
39
+ return Token.parse_obj(res)
40
+ except BadRequestException as ex:
41
+ raise Exception(
42
+ f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
43
+ ) from ex
44
+
45
+ def get_device_code(self) -> DeviceCode:
46
+ url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/device"
47
+ data = {"tenantName": self._tenant_name}
48
+ res = requests.post(url, data=data)
49
+ res = request_handling(res)
50
+ return DeviceCode.parse_obj(res)
51
+
52
+ def get_token_from_device_code(
53
+ self, device_code: str, timeout: float = 60
54
+ ) -> Token:
55
+ url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/device/token"
56
+ data = {
57
+ "tenantName": self._tenant_name,
58
+ "deviceCode": device_code,
59
+ }
60
+ start_time = time.monotonic()
61
+ poll_interval_seconds = 1
62
+
63
+ for response in poll_for_function(
64
+ requests.post, poll_after_secs=poll_interval_seconds, url=url, data=data
65
+ ):
66
+ if response.status_code == 201:
67
+ response = response.json()
68
+ return Token.parse_obj(response)
69
+ elif response.status_code == 202:
70
+ logger.debug("User has not authorized yet. Checking again.")
71
+ else:
72
+ raise Exception(
73
+ "Failed to get token using device code. "
74
+ f"status_code {response.status_code},\n {response.text}"
75
+ )
76
+ time_elapsed = time.monotonic() - start_time
77
+ if time_elapsed > timeout:
78
+ logger.warning("Polled server for %s secs.", int(time_elapsed))
79
+ break
80
+
81
+ raise Exception(f"Did not get authorized within {timeout} seconds.")