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

Introduce API for granular (Sensor)Reader operation #33

Merged
merged 4 commits into from
Dec 6, 2022

Conversation

benthorner
Copy link
Contributor

@benthorner benthorner commented Nov 26, 2022

Original PR: #32

The current reader classes aren't optimal for some usecases.
For example, it's hard to "plug in" a SensorReader in this code:

class DataSource:
    def start(self) -> None:
        # prepare to poll for data

    def poll(self) -> Data:
        # return a single data sample (or fail)

    def stop(self) -> None:
        # close down the source

In particular:

  1. The current classes make it hard to separate management of
    resources ("start" / "stop") from getting data ("poll").

  2. The current classes mix CLI-oriented error handling (example)
    with the lower-level concern of just getting the data.

This PR reorganises some of the reader code to address (2) and
exposes more of these internals as a set of new "open", "close"
and "read_one" methods for more granular control to address (1).

@benthorner benthorner changed the title Read one Introduce new API for granular (Sensor)Reader operation Nov 26, 2022
@benthorner benthorner changed the title Introduce new API for granular (Sensor)Reader operation Introduce API for granular (Sensor)Reader operation Nov 26, 2022
Copy link
Owner

@avaldebe avaldebe left a comment

Choose a reason for hiding this comment

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

I really appreciate this PR and see potential on the use case it implements.

I'm concerned about the additional complexity it introduce, so I ask you to be patient with me as I try to figure out a good balance between complexity and conciseness.

docs/library_usage.md Outdated Show resolved Hide resolved
Comment on lines 99 to 115
@dataclass
class _Reading:
"""Represents a single sensor reading

This class is only used by internal functions as a way of passing reading data between them.
"""

buffer: bytes
obs_data: ObsData

@property
def raw_data(self) -> RawData:
return RawData(self.time, self.buffer)

@property
def time(self) -> int:
return self.obs_data.time
Copy link
Owner

Choose a reason for hiding this comment

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

Is this internal class really needed?

Copy link
Contributor Author

@benthorner benthorner Nov 28, 2022

Choose a reason for hiding this comment

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

I use classes because they give meaning to the data being passed around. For example, what is a (buffer, obs_data)? It's not obvious without a class to clarify that it represents the data from a Reading.

That said, I don't feel strongly about it as this is an internal method. We could have this instead:

def _read_one(...):
    ...
    return (buffer, obs_data)

...

def __call__(...):
    ...
    (buffer, obs_data) = self._read_one(...)
    ...
    yield RawData(obs_data.time, buffer) if raw else obs_data

What do you think?

(the construction of RawData would also have to be duplicated in read_one)

src/pms/core/reader.py Outdated Show resolved Hide resolved
avaldebe added a commit that referenced this pull request Nov 28, 2022
avaldebe added a commit that referenced this pull request Nov 28, 2022
avaldebe added a commit that referenced this pull request Nov 28, 2022
Comment on lines 205 to 228
def test_sensor_reader_read_one_warm_up(
mock_sensor,
sensor_reader_factory,
mock_sensor_warm_up,
):
sensor_reader = sensor_reader_factory()

with sensor_reader as r:
with pytest.raises(pms.SensorNotReady):
sensor_reader.read_one()


def test_sensor_reader_read_one_temp_failure(
mock_sensor,
sensor_reader_factory,
mock_sensor_temp_failure,
):
sensor_reader = sensor_reader_factory()

with sensor_reader as r:
with pytest.raises(pms.SensorWarning):
sensor_reader.read_one()


Copy link
Owner

Choose a reason for hiding this comment

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

what is the purpose of read_one?
If it is to read one observation, these exceptions should be addressed internally and ask the sensor for an new message (silently).

If it is meant to request one observation (and let the caller handle the retry),
then this test are appropriate.

I think that read_one should deliver one observation, independently of how many samples are requested from the sensor and discarded for different reasons.

If you want the caller to handle the retries, I could add a max_tries option to read_one and __call__. This way when the retry exceeds max_tries the exception will bubble up to the caller.

Copy link
Contributor Author

@benthorner benthorner Dec 2, 2022

Choose a reason for hiding this comment

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

If it is meant to request one observation (and let the caller handle the retry)

Yes, that's right: that's what I need it to do.

I had some questions, but then thought it would be better to share an example. I've appended some extra commits, which are what I think you're aiming for with max_(re)tries and read_one.

The penultimate commit isn't great. I'd argue that's why read_one and ultimately Streams are better, because it avoids confusion over when samples, interval and (now) max_retries are relevant. Given where we are, it would be clearer to drop that commit and for me to just do next(reader()) manually.

Anyway, how does it look?

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, this is what I had in mind. Thanks for the implementation.
I'll merge this PR as soon as I you confirm that you're done with it.

Thanks again,
Á.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be ready for another look now...

This will give us more confidence when we refactor these exception
handlers in the next commits.
This clarifies that both exceptions relate to a temporary condition
where it makes sense to wait before trying again.
In response to [^1].

Previously it was hard to operate the reader classes from calling
code that separates resource management ("open" / "close") and the
actual polling of data ("read_one"). This exposes new APIs to make
it easier to use reader classes in such a granular way.

Implementation notes:

- Reinstating a type hint for __enter__ of "-> Self" will become
possible in Python 3.11.

- "start" / "stop" were discussed in relation to the Sensor being
operated by a SensorReader, but these concepts don't make much sense
for the MessageReader class, so I've used "open" / "close" instead.

[^1]: avaldebe#32 (comment)
benthorner added a commit to benthorner/PyPMS that referenced this pull request Dec 2, 2022
This resets the code to allow experimentation with a different API
implementation for [^1].

[^1]: avaldebe#33 (comment)
benthorner added a commit to benthorner/PyPMS that referenced this pull request Dec 2, 2022
This resets the code to allow experimentation with a different API
implementation for [^1].

[^1]: avaldebe#33 (comment)
benthorner added a commit to benthorner/PyPMS that referenced this pull request Dec 2, 2022
This resets the code to allow experimentation with a different API
implementation for [^1].

[^1]: avaldebe#33 (comment)
@benthorner benthorner force-pushed the read-one branch 3 times, most recently from 3b1acea to cbf4d24 Compare December 5, 2022 22:59
@avaldebe avaldebe merged commit 90e904d into avaldebe:master Dec 6, 2022
benthorner added a commit to benthorner/snsary that referenced this pull request Dec 6, 2022
benthorner added a commit to benthorner/snsary that referenced this pull request Dec 6, 2022
benthorner added a commit to benthorner/snsary that referenced this pull request Dec 9, 2022
This is viable since [^1].

There are some minor differences to note:

- SensorReader._cmd uses Serial.flush(), but this shouldn't be an
issue in newer versions of Python [^2].

- SensorReader._cmd reads all available input into the buffer and
only calls Serial.reset after a "_read", or if the buffer turns out
to be invalid. This is less robust than just calling Serial.reset
before each command, but it seems to work.

- SensorReader.open does an additional check to verify the sensor.

[^1]: avaldebe/PyPMS#33
[^2]: python/cpython#97001
benthorner added a commit to benthorner/snsary that referenced this pull request Dec 9, 2022
This is viable since [^1].

There are some minor differences to note:

- SensorReader._cmd uses Serial.flush(), but this shouldn't be an
issue in newer versions of Python [^2].

- SensorReader._cmd reads all available input into the buffer and
only calls Serial.reset after a "_read", or if the buffer turns out
to be invalid. This is less robust than just calling Serial.reset
before each command, but it seems to work.

- SensorReader.open does an additional check to verify the sensor.

[^1]: avaldebe/PyPMS#33
[^2]: python/cpython#97001
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants