Skip to content
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

add env loading #649

Closed

Conversation

stan-dot
Copy link
Contributor

No description provided.

@stan-dot stan-dot added enhancement New feature or request cli Relates to CLI code labels Sep 25, 2024
@stan-dot stan-dot self-assigned this Sep 25, 2024
@stan-dot stan-dot linked an issue Sep 25, 2024 that may be closed by this pull request
@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

slightly unclear where should the default values come from

#495 (comment)

at the moment the ConfigLoader just starts with all is none and gets updated through the file first

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

this needs to be changed because at the moment the tests are failing because of the config loading error@

https://github.com/DiamondLightSource/blueapi/actions/runs/11071605310/job/30763855823?pr=649

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

regarding the run sleep plan test, the client._config.port and host give localhost:8000, so the loading is not correct

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

harder to test, as the debugger does not pick up the client changes. they do not propagate correctly to the client object from the CLI, though config is called

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

OTOH the stomp-loading error is bizarre, and the tkagg is nowhere to be found

File "/workspaces/blueapi/tests/unit_tests/test_cli.py", line 170, in test_valid_stomp_config_for_listener
    assert result.output == dedent("""\
AssertionError: assert 'Backend tkagg is interactive backend. Turning interactive mode on.\n' == 'Subscribing to all bluesky events from localhost:61613\nPress enter to exit\n'
  
  + Backend tkagg is interactive backend. Turning interactive mode on.
  - Subscribing to all bluesky events from localhost:61613
  - Press enter to exit

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

for the third of the failing tests, maybe the config is not loaded fully before the assertion is called?

let's check if the cli:main line for use_values_from_yaml is called when debugging the test

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

it must be the cli OR env values that override (incorrectly) the prior stomp config

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

not quite, it's not overriding, it's just that the use_values does not succeed in mutating the ConfigLoader state

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

I forgot the assignment after making a pure function:

        self._values = recursively_updated_map(self._values, values)

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

ok actually the last test might be failing due to other kinds of config being invalid, not the scratch field:

Type: string_type, Location: stomp -> host, Message: Input should be a valid string, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/string_type
Type: int_type, Location: stomp -> port, Message: Input should be a valid integer, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/int_type
Type: enum, Location: env -> sources -> 0 -> kind, Message: Input should be 'planFunctions', 'deviceFunctions' or 'dodal', Input: None, Documentation: https://errors.pydantic.dev/2.9/v/enum
Type: path_type, Location: env -> sources -> 0 -> module -> lax-or-strict[lax=union[json-or-python[json=function-after[path_validator(), str],python=is-instance[Path]],function-after[path_validator(), str]],strict=json-or-python[json=function-after[path_validator(), str],python=is-instance[Path]]], Message: Input is not a valid path for <class 'pathlib.Path'>, Input: None
Type: string_type, Location: env -> sources -> 0 -> module -> str, Message: Input should be a valid string, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/string_type
Type: bool_type, Location: env -> events -> broadcast_status_events, Message: Input should be a valid boolean, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/bool_type
Type: literal_error, Location: logging -> level, Message: Input should be 'NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL', Input: None, Documentation: https://errors.pydantic.dev/2.9/v/literal_error
Type: string_type, Location: api -> host, Message: Input should be a valid string, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/string_type
Type: int_type, Location: api -> port, Message: Input should be a valid integer, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/int_type
Type: string_type, Location: api -> protocol, Message: Input should be a valid string, Input: None, Documentation: https://errors.pydantic.dev/2.9/v/string_type

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

thus it comes back to the question of where does the default config come from - which is - the config.py hardcoded values, such as:

class RestConfig(BlueapiBaseModel):
    host: str = "localhost"
    port: int = 8000
    protocol: str = "http"

stan-dot pushed a commit that referenced this pull request Oct 7, 2024
#649 @stan-dot 

For the environment to work, it needed to be in dictionary from so a
string to dictionary conversion is needed. This was me playing with it.

You probably already working on it, the cli_value are all none unless
you assign them so pydantic keep complaining.
@Relm-Arrowny
Copy link
Contributor

thus it comes back to the question of where does the default config come from - which is - the config.py hardcoded values, such as:

class RestConfig(BlueapiBaseModel):
    host: str = "localhost"
    port: int = 8000
    protocol: str = "http"

Not sure, if this is the answer you looking for but these are default value inside uvicorn so if you leave it blank when you do run these will be the default value so I guess we just bring it up to so it changeable.

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 7, 2024

thus it comes back to the question of where does the default config come from - which is - the config.py hardcoded values, such as:

class RestConfig(BlueapiBaseModel):
    host: str = "localhost"
    port: int = 8000
    protocol: str = "http"

Not sure, if this is the answer you looking for but these are default value inside uvicorn so if you leave it blank when you do run these will be the default value so I guess we just bring it up to so it changeable.

Cool, I didn't realize the uvicorn connection, thanks

Copy link

codecov bot commented Oct 7, 2024

Codecov Report

Attention: Patch coverage is 96.61017% with 2 lines in your changes missing coverage. Please review.

Project coverage is 92.76%. Comparing base (cd91f0f) to head (dc9577c).

Files with missing lines Patch % Lines
src/blueapi/config.py 95.55% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #649      +/-   ##
==========================================
+ Coverage   92.62%   92.76%   +0.14%     
==========================================
  Files          35       35              
  Lines        1654     1700      +46     
==========================================
+ Hits         1532     1577      +45     
- Misses        122      123       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@stan-dot stan-dot marked this pull request as ready for review October 7, 2024 16:15
@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 8, 2024

yay it works!

src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/config.py Outdated Show resolved Hide resolved
src/blueapi/config.py Outdated Show resolved Hide resolved
src/blueapi/config.py Outdated Show resolved Hide resolved
src/blueapi/config.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
tests/unit_tests/test_cli.py Outdated Show resolved Hide resolved
@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 9, 2024

Thanks @callumforrester for the review, I'll implement shortly

Copy link
Contributor

@tpoliaw tpoliaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely needs docs on how to use this. I can't see a way of passing options to the CLI that aren't blocked as unrecognised by click. It would also be good to have a list of options that are expected or available

src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
for key, value in os.environ.items():
if key.startswith(prefix):
# Convert key to a config path-like structure
config_key = key[len(prefix) :].lower()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
config_key = key[len(prefix) :].lower()
config_key = key.removeprefix(prefix).lower()

Should this also use the same method as the CLI parsing? eg BLUEAPI_CONFIG_FOO=bar sets values['config']['foo'] = "bar" instead of relying on eval (which presumably requires something like BLUEAPI.CONFIG='{"foo": "bar"}')?

tests/unit_tests/test_config.py Outdated Show resolved Hide resolved
src/blueapi/cli/cli.py Outdated Show resolved Hide resolved
src/blueapi/config.py Outdated Show resolved Hide resolved
raise InvalidConfigError(
"Something is wrong with the configuration file: \n"
f"""Something is wrong with the configuration file:
{pretty_error_messages}"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this end up with the first error being weirdly indented?

new = {"a": {"y": 20, "z": 30}, "c": 4}
expected = {"a": {"x": 1, "y": 20, "z": 30}, "b": 3, "c": 4}
result = recursively_updated_map(old, new)
assert result == expected, f"Expected {expected}, but got {result}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also test that old wasn't modified?

stan-dot pushed a commit that referenced this pull request Oct 16, 2024
#649 @stan-dot 

For the environment to work, it needed to be in dictionary from so a
string to dictionary conversion is needed. This was me playing with it.

You probably already working on it, the cli_value are all none unless
you assign them so pydantic keep complaining.
@stan-dot stan-dot force-pushed the 495-set-configuration-options-via-environment-variables branch from d28ee9a to 6213805 Compare October 28, 2024 11:40
@stan-dot stan-dot force-pushed the 495-set-configuration-options-via-environment-variables branch from 6213805 to 138a096 Compare October 29, 2024 15:12
@stan-dot
Copy link
Contributor Author

actually there should be a package for that

@stan-dot
Copy link
Contributor Author

stan-dot commented Oct 31, 2024

@DominicOram please take a look at the examples for this https://github.com/PasaOpasen/py-env-parser?tab=readme-ov-file#examples

if it's not right, the logic is just 200 lines of code so we could copy paste into this repo and adjust should we want to modulate the API - but arguably this would be useful across DLS

@Relm-Arrowny
Copy link
Contributor

Relm-Arrowny commented Oct 31, 2024

@DominicOram please take a look at the examples for this https://github.com/PasaOpasen/py-env-parser?tab=readme-ov-file#examples

if it's not right, the logic is just 200 lines of code so we could copy paste into this repo and adjust should we want to modulate the API - but arguably this would be useful across DLS

Not sure if this help or not, I think pydantic has something call setting management that allow you to pick up env variable if it is missing. and you can just do something like

BaseSettings().dict() 

to get the dictionary out. But of cause you will have to already have a pydantic baseModel for the data you want to parse.

@callumforrester
Copy link
Contributor

@Relm-Arrowny it's worth looking into but I'm not sure how well it works for nested data

@stan-dot
Copy link
Contributor Author

@callumforrester it promises a pretty good level of support. https://docs.pydantic.dev/latest/concepts/pydantic_settings/#nested-model-default-partial-updates

thanks @Relm-Arrowny for finding this, as a feature of pydantic it looks more solid than the library I found

@DominicOram
Copy link
Contributor

@DominicOram please take a look at the examples for this https://github.com/PasaOpasen/py-env-parser?tab=readme-ov-file#examples

if it's not right, the logic is just 200 lines of code so we could copy paste into this repo and adjust should we want to modulate the API - but arguably this would be useful across DLS

As just a user of BlueAPI I don't have strong or well-informed opinions about implementation details and I feel like any opinion I do give will just muddy the water. So I defer to @callumforrester and @tpoliaw as the maintainers of BlueAPI to make any decisions around this issue.

@stan-dot
Copy link
Contributor Author

stan-dot commented Nov 4, 2024

closing as the implementation with pydantic will be entirely different

https://docs.pydantic.dev/latest/concepts/pydantic_settings/#parsing-environment-variable-values

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cli Relates to CLI code enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Set configuration options via environment variables
6 participants