-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add rest config and rest config checker #2432
base: develop
Are you sure you want to change the base?
Conversation
b620c8c
to
e7536bf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some refactoring is needed. You can have a look at the taipy/core/config/__init__.py
and taipy/core/config/core_section.py
files
from taipy.common.config import Section, _inject_section | ||
|
||
|
||
class RestConfig(Section): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be a unique section, no? Only one RestConfig should be created in the Config. No?
def ssl_context(self) -> Optional[Tuple[Optional[str], Optional[str]]]: | ||
return (self._ssl_cert, self._ssl_key) if self._use_https else None | ||
|
||
def configure_rest( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use the same design pattern as the existing section implementations for legacy and consistency, even if your proposal seems smoother. Changing the design pattern could be a good idea (the factory pattern can be cumbersome), but it requires some deep discussions before, as it is an important breaking change.
For the other sections, we reserve the __init__
constructors for Taipy's internal usage. It is indirectly used by serializers (TOML and JSON) that need to instantiate sections from files (eventually with some missing attributes). So they expose all the parameters as optional. On the contrary, the _configure
method is a static method acting as a factory, so the developer doesn't need to use the constructor directly (__init__
method). It instantiates the Section, registers it in the Config, and returns it.
Something like:
@staticmethod
def _configure(
port: int = 5000,
host: str = "127.0.0.1",
use_https: bool = False,
ssl_cert: Optional[str] = None,
ssl_key: Optional[str] = None,
):
section = RestConfig(port, host, use_https, ssl_cert, ssl_key)
Config._register(section)
return Config.unique_sections[RestConfig.name]
_inject_section( | ||
RestConfig, "rest", default=RestConfig(), configuration_methods=[("configure_rest", RestConfig.configure_rest)] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually do it in the __init__.py
file as the checker must also be added.
# | ||
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||
# specific language governing permissions and limitations under the License. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# specific language governing permissions and limitations under the License. | |
# specific language governing permissions and limitations under the License. | |
_inject_section( | |
RestConfig, | |
"rest", | |
RestConfig.default_config(), | |
[("configure_rest", RestConfig._configure)], | |
add_to_unconflicted_sections=True, | |
) | |
_Checker.add_checker(_RestConfigChecker) |
|
||
|
||
class RestConfig(Section): | ||
name: str = "REST" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name: str = "REST" | |
"""Configuration parameters for running the `Rest^` service.""" | |
name: str = "REST" |
|
||
@classmethod | ||
def _to_dict(cls): | ||
return {"port": cls.port, "host": cls.host, "use_https": cls.use_https, "ssl_cert": cls.ssl_cert} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If some values are None, they must not be added to the dict.
instance.port = data.get("port", 5000) | ||
instance.host = data.get("host", "127.0.0.1") | ||
instance.use_https = data.get("use_https", False) | ||
instance.ssl_cert = data.get("ssl_cert", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot set the default values here when an attribute is not in the dictionary. We must use None instead. Otherwise, the compilation mechanism won't work when the user overrides only one attribute from a TOML file (or from an env variable)
return instance | ||
|
||
@property | ||
def port(self) -> int: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the getters should use the templating system to allow users to overwrite values from env variables. Use _tpl._replace_templates
that should do the job.
super().__init__(config, collector) | ||
|
||
def _check(self) -> IssueCollector: | ||
rest_configs = cast(dict, self._config._sections.get(RestConfig.name, {})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the RestConfig is a unique section, this line should be simpler.
|
||
|
||
def test_rest_config_default_values(): | ||
rest_config = RestConfig() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not need to do anything to retrieve the default values.
Then when the configure method is called, it should overwrite the values.
Then when loading a TOML file, this should again overwrite the values.
And the same with the env variables:
Config.rest.port = RestConfig._DEFAULT_PORT
Config.rest.host = RestConfig._DEFAULT_HOST
... # etc for all the attributes
# Now we use the configure method
Config.configure_rest(port=1)
Config.rest.port = 1
Config.rest.host = RestConfig._DEFAULT_HOST
# now we load a toml file
toml_cfg = NamedTemporaryFile(
content="""
[TAIPY]
[REST]
port = 2
host = "192.168.0.87"
"""
)
Config.load(toml_cfg.filename)
Config.rest.port = 2
Config.rest.host = "192.168.0.87"
# And finally we use env variables
assert Config.rest.port == RestConfig._DEFAULT_PORT
assert Config.rest.host== RestConfig._DEFAULT_HOST
Config.configure_rest(port="ENV[PORT]", host="ENV[HOST]")
with pytest.raises(MissingEnvVariableError):
_ = Config.rest.port
with pytest.raises(MissingEnvVariableError):
_ = Config.rest.host
... # etc for all the attributes
with patch.dict(os.environ, {"PORT": "3:int", "HOST": "1.2.3.4"}):
assert Config.rest.port == 3
assert Config.rest.bar == "1.2.3.4"
What type of PR is this? (check all applicable)
Description
Add option to inform rest server Host, Port and other attributes that could be used for a deploy in a production environment.
Related Tickets & Documents
Checklist
We encourage you to keep the code coverage percentage at 80% and above.