File size: 11,627 Bytes
105b369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
from typing import Optional, Dict, Any, Union, List

from pydantic import field_validator, Field
from pydantic_core.core_schema import FieldValidationInfo

from phi.base import PhiBase
from phi.app.context import ContainerContext
from phi.resource.base import ResourceBase
from phi.utils.log import logger


class AppBase(PhiBase):
    # -*- App Name (required)
    name: str

    # -*- Image Configuration
    # Image can be provided as a DockerImage object
    image: Optional[Any] = None
    # OR as image_name:image_tag str
    image_str: Optional[str] = None
    # OR as image_name and image_tag
    image_name: Optional[str] = None
    image_tag: Optional[str] = None
    # Entrypoint for the container
    entrypoint: Optional[Union[str, List[str]]] = None
    # Command for the container
    command: Optional[Union[str, List[str]]] = None

    # -*- Python Configuration
    # Install python dependencies using a requirements.txt file
    install_requirements: bool = False
    # Path to the requirements.txt file relative to the workspace_root
    requirements_file: str = "requirements.txt"
    # Set the PYTHONPATH env var
    set_python_path: bool = True
    # Manually provide the PYTHONPATH.
    # If None, PYTHONPATH is set to workspace_root
    python_path: Optional[str] = None
    # Add paths to the PYTHONPATH env var
    # If python_path is provided, this value is ignored
    add_python_paths: Optional[List[str]] = None

    # -*- App Ports
    # Open a container port if open_port=True
    open_port: bool = False
    # If open_port=True, port_number is used to set the
    # container_port if container_port is None and host_port if host_port is None
    port_number: int = 80
    # Port number on the Container to open
    # Preferred over port_number if both are set
    container_port: Optional[int] = Field(None, validate_default=True)
    # Port name for the opened port
    container_port_name: str = "http"
    # Port number on the Host to map to the Container port
    # Preferred over port_number if both are set
    host_port: Optional[int] = Field(None, validate_default=True)

    # -*- Extra Resources created "before" the App resources
    resources: Optional[List[ResourceBase]] = None

    #  -*- Other args
    print_env_on_load: bool = False

    # -*- App specific args. Not to be set by the user.
    # Container Environment that can be set by subclasses
    # which is used as a starting point for building the container_env
    # Any variables set in container_env will be overriden by values
    # in the env_vars dict or env_file
    container_env: Optional[Dict[str, Any]] = None
    # Variable used to cache the container context
    container_context: Optional[ContainerContext] = None

    # -*- Cached Data
    cached_resources: Optional[List[Any]] = None

    @field_validator("container_port", mode="before")
    def set_container_port(cls, v, info: FieldValidationInfo):
        port_number = info.data.get("port_number")
        if v is None and port_number is not None:
            v = port_number
        return v

    @field_validator("host_port", mode="before")
    def set_host_port(cls, v, info: FieldValidationInfo):
        port_number = info.data.get("port_number")
        if v is None and port_number is not None:
            v = port_number
        return v

    def get_app_name(self) -> str:
        return self.name

    def get_image_str(self) -> str:
        if self.image:
            return f"{self.image.name}:{self.image.tag}"
        elif self.image_str:
            return self.image_str
        elif self.image_name and self.image_tag:
            return f"{self.image_name}:{self.image_tag}"
        elif self.image_name:
            return f"{self.image_name}:latest"
        else:
            return ""

    def build_resources(self, build_context: Any) -> Optional[Any]:
        logger.debug(f"@build_resource_group not defined for {self.get_app_name()}")
        return None

    def get_dependencies(self) -> Optional[List[ResourceBase]]:
        return (
            [dep for dep in self.depends_on if isinstance(dep, ResourceBase)] if self.depends_on is not None else None
        )

    def add_app_properties_to_resources(self, resources: List[ResourceBase]) -> List[ResourceBase]:
        updated_resources = []
        app_properties = self.model_dump(exclude_defaults=True)
        app_group = self.get_group_name()
        app_output_dir = self.get_app_name()

        app_skip_create = app_properties.get("skip_create", None)
        app_skip_read = app_properties.get("skip_read", None)
        app_skip_update = app_properties.get("skip_update", None)
        app_skip_delete = app_properties.get("skip_delete", None)
        app_recreate_on_update = app_properties.get("recreate_on_update", None)
        app_use_cache = app_properties.get("use_cache", None)
        app_force = app_properties.get("force", None)
        app_debug_mode = app_properties.get("debug_mode", None)
        app_wait_for_create = app_properties.get("wait_for_create", None)
        app_wait_for_update = app_properties.get("wait_for_update", None)
        app_wait_for_delete = app_properties.get("wait_for_delete", None)
        app_save_output = app_properties.get("save_output", None)

        for resource in resources:
            resource_properties = resource.model_dump(exclude_defaults=True)
            resource_skip_create = resource_properties.get("skip_create", None)
            resource_skip_read = resource_properties.get("skip_read", None)
            resource_skip_update = resource_properties.get("skip_update", None)
            resource_skip_delete = resource_properties.get("skip_delete", None)
            resource_recreate_on_update = resource_properties.get("recreate_on_update", None)
            resource_use_cache = resource_properties.get("use_cache", None)
            resource_force = resource_properties.get("force", None)
            resource_debug_mode = resource_properties.get("debug_mode", None)
            resource_wait_for_create = resource_properties.get("wait_for_create", None)
            resource_wait_for_update = resource_properties.get("wait_for_update", None)
            resource_wait_for_delete = resource_properties.get("wait_for_delete", None)
            resource_save_output = resource_properties.get("save_output", None)

            # If skip_create on resource is not set, use app level skip_create (if set on app)
            if resource_skip_create is None and app_skip_create is not None:
                resource.skip_create = app_skip_create
            # If skip_read on resource is not set, use app level skip_read (if set on app)
            if resource_skip_read is None and app_skip_read is not None:
                resource.skip_read = app_skip_read
            # If skip_update on resource is not set, use app level skip_update (if set on app)
            if resource_skip_update is None and app_skip_update is not None:
                resource.skip_update = app_skip_update
            # If skip_delete on resource is not set, use app level skip_delete (if set on app)
            if resource_skip_delete is None and app_skip_delete is not None:
                resource.skip_delete = app_skip_delete
            # If recreate_on_update on resource is not set, use app level recreate_on_update (if set on app)
            if resource_recreate_on_update is None and app_recreate_on_update is not None:
                resource.recreate_on_update = app_recreate_on_update
            # If use_cache on resource is not set, use app level use_cache (if set on app)
            if resource_use_cache is None and app_use_cache is not None:
                resource.use_cache = app_use_cache
            # If force on resource is not set, use app level force (if set on app)
            if resource_force is None and app_force is not None:
                resource.force = app_force
            # If debug_mode on resource is not set, use app level debug_mode (if set on app)
            if resource_debug_mode is None and app_debug_mode is not None:
                resource.debug_mode = app_debug_mode
            # If wait_for_create on resource is not set, use app level wait_for_create (if set on app)
            if resource_wait_for_create is None and app_wait_for_create is not None:
                resource.wait_for_create = app_wait_for_create
            # If wait_for_update on resource is not set, use app level wait_for_update (if set on app)
            if resource_wait_for_update is None and app_wait_for_update is not None:
                resource.wait_for_update = app_wait_for_update
            # If wait_for_delete on resource is not set, use app level wait_for_delete (if set on app)
            if resource_wait_for_delete is None and app_wait_for_delete is not None:
                resource.wait_for_delete = app_wait_for_delete
            # If save_output on resource is not set, use app level save_output (if set on app)
            if resource_save_output is None and app_save_output is not None:
                resource.save_output = app_save_output
            # If workspace_settings on resource is not set, use app level workspace_settings (if set on app)
            if resource.workspace_settings is None and self.workspace_settings is not None:
                resource.set_workspace_settings(self.workspace_settings)
            # If group on resource is not set, use app level group (if set on app)
            if resource.group is None and app_group is not None:
                resource.group = app_group

            # Always set output_dir on resource to app level output_dir
            resource.output_dir = app_output_dir

            app_dependencies = self.get_dependencies()
            if app_dependencies is not None:
                if resource.depends_on is None:
                    resource.depends_on = app_dependencies
                else:
                    resource.depends_on.extend(app_dependencies)

            updated_resources.append(resource)
        return updated_resources

    def get_resources(self, build_context: Any) -> List[ResourceBase]:
        if self.cached_resources is not None and len(self.cached_resources) > 0:
            return self.cached_resources

        base_resources = self.resources or []
        app_resources = self.build_resources(build_context)
        if app_resources is not None:
            base_resources.extend(app_resources)

        self.cached_resources = self.add_app_properties_to_resources(base_resources)
        # logger.debug(f"Resources: {self.cached_resources}")
        return self.cached_resources

    def matches_filters(self, group_filter: Optional[str] = None) -> bool:
        if group_filter is not None:
            group_name = self.get_group_name()
            logger.debug(f"{self.get_app_name()}: Checking {group_filter} in {group_name}")
            if group_name is None or group_filter not in group_name:
                return False
        return True

    def should_create(self, group_filter: Optional[str] = None) -> bool:
        if not self.enabled or self.skip_create:
            return False
        return self.matches_filters(group_filter)

    def should_delete(self, group_filter: Optional[str] = None) -> bool:
        if not self.enabled or self.skip_delete:
            return False
        return self.matches_filters(group_filter)

    def should_update(self, group_filter: Optional[str] = None) -> bool:
        if not self.enabled or self.skip_update:
            return False
        return self.matches_filters(group_filter)