-
-
Notifications
You must be signed in to change notification settings - Fork 1
Change simulation encoding workaround to official rocketpy encoders #51
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
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Warning Rate limit exceeded@GabrielBarberini has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 6 minutes and 3 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (8)
WalkthroughReplaces generic attribute extraction with a RocketPy-aware encoder (rocketpy_encoder + DiscretizeConfig) across services; updates simulation view models to accept dynamically encoded attributes; changes serialization to omit None fields in API models/views and repository payloads. Changes
Sequence Diagram(s)sequenceDiagram
participant Service as Service (e.g., FlightService)
participant Obj as RocketPy Object
participant Config as DiscretizeConfig
participant Encoder as rocketpy_encoder
participant View as SimulationView
Service->>Obj: read instance (self.<obj>)
Service->>Config: DiscretizeConfig.for_<type>()
Service->>Encoder: rocketpy_encoder(Obj, Config)
note right of Encoder #eef: copy object, discretize Function attributes\nsample functions within bounds/samples
Encoder->>Encoder: serialize copy via RocketPyEncoder (include functions/outputs)
Encoder-->>Service: return encoded dict
Service->>View: instantiate SimulationView(**encoded dict)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Co-authored-by: Copilot <[email protected]>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
Pull Request Overview
This PR modernizes the simulation encoding approach by replacing the deprecated get_instance_attributes utility with official RocketPy encoders and introducing a new DiscretizeConfig class for better dynamic attribute handling. The changes enable more flexible and extensible simulation data serialization across all RocketPy object types.
Key Changes:
- Replaced
get_instance_attributeswith newrocketpy_encoderfunction using official RocketPyRocketPyEncoder - Added
DiscretizeConfigclass for configurable function discretization parameters - Updated all simulation view classes to use Pydantic's
ConfigDictwithextra='allow'for dynamic attributes
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils.py | Added DiscretizeConfig class and rocketpy_encoder function replacing deprecated encoding approach |
| src/views/*.py | Updated simulation view classes to use ConfigDict and replaced specific attribute types with Any |
| src/services/*.py | Replaced get_instance_attributes calls with rocketpy_encoder using appropriate configs |
| src/repositories/*.py | Added exclude_none=True to model_dump() calls for cleaner serialization |
| src/models/interface.py | Added ser_json_exclude_none=True to base model configuration |
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.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/models/interface.py(1 hunks)src/repositories/flight.py(1 hunks)src/repositories/motor.py(1 hunks)src/repositories/rocket.py(1 hunks)src/views/interface.py(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-02-22T21:53:21.139Z
Learnt from: GabrielBarberini
PR: RocketPy-Team/Infinity-API#44
File: src/models/interface.py:12-17
Timestamp: 2025-02-22T21:53:21.139Z
Learning: In Pydantic v2, both bare dict and ConfigDict can be used for model_config, but ConfigDict is recommended for better type hints and IDE support.
Applied to files:
src/views/interface.py
📚 Learning: 2025-02-22T21:53:21.139Z
Learnt from: GabrielBarberini
PR: RocketPy-Team/Infinity-API#44
File: src/models/interface.py:12-17
Timestamp: 2025-02-22T21:53:21.139Z
Learning: Pydantic v2 requires using ConfigDict for model configuration. Plain dictionaries lack type safety, proper validation, and full feature support.
Applied to files:
src/views/interface.py
🧬 Code Graph Analysis (3)
src/repositories/rocket.py (4)
src/repositories/interface.py (2)
insert(225-232)update_by_id(235-239)src/models/rocket.py (1)
RocketModel(13-63)src/routes/rocket.py (1)
update_rocket(56-68)tests/unit/test_routes/test_rockets_route.py (1)
test_update_rocket(357-365)
src/repositories/flight.py (2)
src/repositories/interface.py (2)
insert(225-232)update_by_id(235-239)src/models/flight.py (2)
FlightModel(7-71)DELETED(53-54)
src/repositories/motor.py (5)
src/repositories/interface.py (3)
insert(225-232)find_by_id(242-249)update_by_id(235-239)src/models/motor.py (2)
MotorModel(16-95)UPDATED(73-74)tests/unit/test_routes/test_motors_route.py (2)
test_update_motor_invalid_input(301-305)test_update_motor(291-298)src/routes/motor.py (1)
update_motor(56-68)tests/unit/test_routes/conftest.py (1)
stub_motor_dump(19-30)
🔇 Additional comments (6)
src/repositories/rocket.py (2)
22-22: Insert payload: LGTMUsing model_dump(exclude_none=True) on create avoids persisting nulls for optional fields and aligns with the new global serialization strategy.
30-32: Re-verify null-field clearing behavior in update_by_idI ran the suggested search for any tests sending
None/nullin update payloads and found no matches. However, absence of tests isn’t evidence that no clients rely on clearing fields vianull. Please confirm whether any API consumers or tests expect setting a field tonullto clear it. If so, address one of the following:
- Keep
exclude_none=Trueand update your API docs/tests to note thatnullcan no longer clear fields.- Restore clearing behavior by splitting the payload into
$setand$unset(example below).File: src/repositories/rocket.py
Lines: 30–32- await self.update_by_id( - rocket.model_dump(exclude_none=True), data_id=rocket_id - ) + raw = rocket.model_dump() + to_unset = {k: "" for k, v in raw.items() if v is None} + to_set = {k: v for k, v in raw.items() if v is not None} + await self.update_by_id( + {"$set": to_set, "$unset": to_unset}, data_id=rocket_id + )src/repositories/flight.py (1)
22-22: Insert payload: consistent and correctexclude_none=True on create is consistent with the new model/view serialization. Good.
src/repositories/motor.py (1)
22-22: Create flow: LGTMexclude_none=True reduces stored document size and matches the PR-wide policy.
src/views/interface.py (1)
1-1: No JSON null expectations in view responses — approving use of ConfigDictThe only
Noneassertion found (tests/unit/test_repositories/test_repository_interface.py:235) pertains to the repository stub’s return value, not to JSON payloads. There are no tests checking for": null"in view output, so omittingnullfields viaser_json_exclude_none=Trueis safe.src/models/interface.py (1)
24-24: BaseModel JSON now omits None globallyThe
ser_json_exclude_none=Trueflag on your BaseModel (src/models/interface.py:24) means that anyNone‐valued fields will be dropped from the JSON output (model_dump_json()/.json()), matching patterns in repos/views.
- I ran a search for any code or tests that inspect explicit
null/Nonevalues in JSON serialization and found none.- Existing calls using
exclude_none=Trueon.dict()remain unaffected.Action required:
- Update your API or developer documentation to note that fields with
Nonevalues will no longer appear in serialized JSON (clients expecting explicitnullentries should be informed).fix_required
| PlainSerializer(to_python_primitive), | ||
| ] | ||
| try: | ||
| json_str = json.dumps( |
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.
@phmbressan @GabrielBarberini not sure who this is for but when i run the env class simulation I get an incorrect date format here
"date": [2025, 6, 9, 23]
get_instance_attributes parses it as a natural datetime object. Is this a encoder issue? Observed this behavior while printing this json_str variable.
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 seems an issue within RocketPyEncoder, I've solved it with a temporary post-processing hack here: 8703230
Co-authored-by: Copilot <[email protected]>
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.
Actionable comments posted: 6
🧹 Nitpick comments (6)
src/views/environment.py (1)
23-39: Clarify “always present” vs Optional typingThe comment says “always present” but fields are Optional[...] with None defaults. Either:
- make truly required (remove Optional and defaults), or
- reword to “commonly present” to match the schema.
src/views/rocket.py (1)
33-34: Specify element types for collectionsParachutes are currently typed as plain list; consider at least list[Any] or a dedicated Parachute view model for better docs and validation.
- parachutes: Optional[list] = None + parachutes: Optional[list[Any]] = Nonesrc/views/motor.py (1)
26-26: Refine dry_inertia type for clarity and toolingUse explicit float tuples to improve schema/docs and static analysis.
- dry_inertia: Optional[tuple] = None + dry_inertia: Optional[tuple[float, float, float]] = Nonesrc/views/flight.py (3)
9-19: Multiple inheritance of Pydantic models: verify MRO and field conflictsFlightSimulation inherits from both RocketSimulation and EnvironmentSimulation. In Pydantic v2, multiple inheritance can lead to field shadowing (e.g., latitude/longitude) and config conflicts. Ensure this composes as intended and no fields silently override each other.
If issues surface, prefer composition:
- class FlightSimulation(ApiBaseView) with fields rocket: RocketSimulation and environment: EnvironmentSimulation, plus flight-specific fields.
34-41: Shadowing environment’s latitude/longitude with flight trajectoriesThese names now represent time-series/derived values (Any), shadowing EnvironmentSimulation’s scalar floats. That’s easy to misinterpret in docs and clients.
Options:
- Rename to latitude_trajectory/longitude_trajectory/altitude_trajectory, or
- Keep names but add clear docstrings on these fields indicating they are trajectories, not environment site coordinates.
30-32: Be explicit about initial_solution element typesinitial_solution is an opaque list; specify element types to improve docs and correctness.
- initial_solution: Optional[list] = None + initial_solution: Optional[list[float]] = NoneIf this can be multi-dimensional, consider list[list[float]] or a TypedDict/NamedTuple for clarity.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/views/environment.py(2 hunks)src/views/flight.py(1 hunks)src/views/motor.py(1 hunks)src/views/rocket.py(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-15T15:12:21.314Z
Learnt from: GabrielBarberini
PR: RocketPy-Team/Infinity-API#38
File: lib/routes/motor.py:75-75
Timestamp: 2024-11-15T15:12:21.314Z
Learning: When modifying API route paths in `lib/routes/motor.py`, changing from `/rocketpy/{motor_id}` to `/{motor_id}/rocketpy` is acceptable when no external consumers are affected. It's acceptable to introduce this breaking change if the team has decided to adopt the new approach, provided that the `README` and related documentation are updated accordingly.
Applied to files:
src/views/motor.py
🧬 Code graph analysis (4)
src/views/rocket.py (4)
src/services/rocket.py (6)
rocket(96-97)rocket(100-101)check_parachute_trigger(220-235)get_rocket_simulation(103-112)RocketService(23-233)from_rocket_model(30-93)src/models/rocket.py (2)
RocketModel(13-63)UPDATED(41-42)src/views/interface.py (1)
ApiBaseView(4-6)src/views/motor.py (2)
MotorView(74-75)MotorSimulation(7-71)
src/views/environment.py (2)
src/views/interface.py (1)
ApiBaseView(4-6)src/services/environment.py (5)
environment(38-39)environment(42-43)EnvironmentService(11-68)from_env_model(18-39)__init__(14-15)
src/views/flight.py (4)
src/services/flight.py (3)
flight(44-45)flight(48-49)FlightService(14-69)src/views/interface.py (1)
ApiBaseView(4-6)src/views/rocket.py (1)
RocketSimulation(8-54)src/views/environment.py (1)
EnvironmentSimulation(8-67)
src/views/motor.py (3)
src/views/interface.py (1)
ApiBaseView(4-6)src/services/motor.py (2)
motor(129-130)motor(133-134)src/models/motor.py (1)
MotorModel(16-95)
🔇 Additional comments (2)
src/views/environment.py (1)
13-17: All services now userocketpy_encoderas documentedI confirmed that:
- No instances of
get_instance_attributes(...)remain in the codebase.- Each service (
rocket.py,motor.py,flight.py,environment.py) invokesrocketpy_encoderwith the correspondingDiscretizeConfig.for_<service>()preset.- The
rocketpy_encoderdefinition insrc/utils.pyis present and correctly referenced by all services.No further action is required here.
src/views/motor.py (1)
39-39: Confirm property naming: interpolate vs interpolation_methodMotorModel uses interpolation_method with a fixed set of literals. If rocketpy_encoder outputs interpolation_method (or a literal), align the name to avoid confusion. If RocketPy’s runtime attribute is actually interpolate, keep it but document the divergence.
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/utils.py (1)
92-103: Fix pipeline failure: remove unusedjson_strand the redundant JSON round-trip.Pylint flags
json_stras unused, and the JSON round-trip is unnecessary since you already returnencoder.default(obj_copy).Apply this diff:
- json_str = json.dumps( - obj_copy, - cls=RocketPyEncoder, - include_outputs=True, - include_function_data=True, - ) - encoder = RocketPyEncoder( + encoder = RocketPyEncoder( include_outputs=True, include_function_data=True, ) return encoder.default(obj_copy)
🧹 Nitpick comments (6)
src/utils.py (6)
7-7: Nit: Prefer built-in generics over typing.Tuple if on Python ≥3.9.If the project targets Python 3.9+, consider using
tuple[float, float]in type hints and droppingTuplefrom imports. If older versions are supported, ignore this.What Python versions does the project officially support (pyproject/poetry.lock/README)? If it's 3.9+, we can modernize hints repo-wide.
17-30: ValidateDiscretizeConfiginputs (bounds order and positive samples).Add lightweight guards so misconfiguration fails fast.
Apply this diff to enforce invariants:
class DiscretizeConfig: @@ - def __init__( - self, bounds: Tuple[float, float] = (0, 10), samples: int = 200 - ): - self.bounds = bounds - self.samples = samples + def __init__( + self, bounds: Tuple[float, float] = (0, 10), samples: int = 200 + ): + if samples <= 0: + raise ValueError("samples must be a positive integer") + if bounds[0] >= bounds[1]: + raise ValueError("bounds must be an increasing (lower, upper) tuple") + self.bounds = bounds + self.samples = samples
48-48: Avoid a mutable default forconfig.Use a None default and instantiate inside to prevent accidental sharing if the object later grows mutable fields.
Apply this diff:
-from typing import NoReturn, Tuple +from typing import NoReturn, Tuple, Optional @@ -def rocketpy_encoder(obj, config: DiscretizeConfig = DiscretizeConfig()): +def rocketpy_encoder(obj, config: Optional[DiscretizeConfig] = None): @@ - # Create a copy to avoid mutating the original object - obj_copy = copy.deepcopy(obj) + # Create a copy to avoid mutating the original object + cfg = config or DiscretizeConfig() + obj_copy = copy.deepcopy(obj)And replace later
config.usages withcfg..
64-66: Performance: deep-copying full RocketPy objects can be heavy.Consider a copy strategy flag (e.g., "deep" | "shallow" | "none") defaulting to "deep", so power users can skip or shallow-copy in performance-sensitive paths.
If you’re open to it, I can follow up with a minimal
copy_strategyparam + docs.
76-87: Preserve Function metadata when discretizing.Reconstructing with
Function(attr_value.source)risks losing metadata (units, interpolation settings, cache, etc.). Prefer cloning the original function and discretizing the clone.Apply this diff:
- if callable(attr_value) and isinstance(attr_value, Function): + if callable(attr_value) and isinstance(attr_value, Function): try: - discretized_func = Function(attr_value.source) - discretized_func.set_discrete( + # Clone original to preserve metadata, then discretize the clone + discretized_func = copy.deepcopy(attr_value) + discretized_func.set_discrete( - lower=config.bounds[0], - upper=config.bounds[1], - samples=config.samples, + lower=cfg.bounds[0], + upper=cfg.bounds[1], + samples=cfg.samples, mutate_self=True, ) setattr(obj_copy, attr_name, discretized_func)If RocketPy provides an official clone/copy API for Function, use that instead of
deepcopy.
104-114: Fallback path currently coerces all values to strings, losing numeric/boolean types.Preserve JSON-native primitives and only stringify non-serializable values to avoid breaking clients.
Apply this diff:
- attributes = {} - for attr_name in dir(obj_copy): - if not attr_name.startswith('_'): - try: - attr_value = getattr(obj_copy, attr_name) - if not callable(attr_value): - attributes[attr_name] = str(attr_value) - except Exception: - continue - return attributes + def _to_json_safe(v): + from datetime import date, datetime + from decimal import Decimal + if isinstance(v, (str, int, float, bool)) or v is None: + return v + if isinstance(v, (date, datetime)): + return v.isoformat() + if isinstance(v, Decimal): + return float(v) + if isinstance(v, (list, tuple)): + return [_to_json_safe(i) for i in v] + if isinstance(v, dict): + return {str(k): _to_json_safe(val) for k, val in v.items()} + return str(v) + + attributes: dict = {} + for attr_name in dir(obj_copy): + if attr_name.startswith('_'): + continue + try: + attr_value = getattr(obj_copy, attr_name) + except Exception: + continue + if callable(attr_value): + continue + attributes[attr_name] = _to_json_safe(attr_value) + return attributesThis keeps numbers/bools as numbers/bools and only stringifies unknowns.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/utils.py(2 hunks)
🧰 Additional context used
🪛 Ruff (0.12.2)
src/utils.py
92-92: Local variable json_str is assigned to but never used
(F841)
🪛 GitHub Actions: Pylint
src/utils.py
[error] 92-92: Pylint: Unused variable 'json_str'. (unused-variable). Command: pylint -d C0415,C0200,C0301,C0114,R0903,C0115,W0246,R0914,C0209,E1121,C0103,C2801,R0801,E1101,E0401,E0611,R0911,C0116,W0212,W0719,W0601,W1203,W0123,W0511,W0621,R0913,R0917 $(git ls-files '*.py')
🔇 Additional comments (5)
src/utils.py (5)
3-5: LGTM on the new imports.
logging,json, andcopyare all used below. No issues here.
9-10: Avoid relying on a private module path (rocketpy._encoders).Leading underscore indicates an internal API that may change without notice. Prefer a public import path if RocketPy exposes one, or pin RocketPy to a version that guarantees
_encoders.RocketPyEncoderexists.Would you check RocketPy’s docs/changelog to confirm if
RocketPyEncoderis publicly exported (and from which module) in the version you pin in this repo?
14-14: LGTM on module-level logger.Good practice for controlled logging across utils.
31-46: Confirm domain defaults and units for discretization presets.
- Environment:
(0, 50000)— meters? altitude?- Motor:
(0, 10)— seconds of burn time?- Rocket:
(0, 1)— nondimensional?- Flight:
(0, 30)— seconds after launch?If these represent time/altitude, ensure they match RocketPy function domains to avoid under/over-sampling artifacts.
Do these presets align with typical ranges we simulate across services (Environment/Motor/Rocket/Flight)? If needed, I can add docstrings per preset to state units and rationale.
142-142: License/attribution check for the forked Starlette GZip code.The comment references a Starlette source file. Ensure BSD-3-Clause attribution requirements are met (license notice retained in this file or centrally in a THIRD_PARTY or LICENSES manifest).
If not already present, add appropriate attribution for Starlette’s gzip middleware fork.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/utils.py (1)
198-274: Fix pipeline failure (pylint R0915: too-many-statements) by refactoring send_with_gzip into small handlers. Also, improve Content-Type check.
- Current method has 55 statements; GH Actions is failing. Split into handlers to reduce complexity.
- Replace the raw header bytes check with a proper Content-Type lookup via Headers.
Apply this diff to slim the dispatcher:
async def send_with_gzip(self, message: Message) -> None: - message_type = message["type"] - if message_type == "http.response.start": - # Don't send the initial message until we've determined how to - # modify the outgoing headers correctly. - self.initial_message = message - headers = Headers(raw=self.initial_message["headers"]) - self.content_encoding_set = "content-encoding" in headers - elif ( - message_type == "http.response.body" and self.content_encoding_set - ): - if not self.started: - self.started = True - await self.send(self.initial_message) - await self.send(message) - elif message_type == "http.response.body" and not self.started: - self.started = True - body = message.get("body", b"") - more_body = message.get("more_body", False) - if ((len(body) < self.minimum_size) and not more_body) or any( - value == b'application/octet-stream' - for header, value in self.initial_message["headers"] - ): - # Don't apply GZip to small outgoing responses or octet-streams. - await self.send(self.initial_message) - await self.send(message) # pylint: disable=unreachable - elif not more_body: - # Standard GZip response. - self.gzip_file.write(body) - self.gzip_file.close() - body = self.gzip_buffer.getvalue() - - headers = MutableHeaders(raw=self.initial_message["headers"]) - headers["Content-Encoding"] = "gzip" - headers["Content-Length"] = str(len(body)) - headers.add_vary_header("Accept-Encoding") - message["body"] = body - - await self.send(self.initial_message) - await self.send(message) # pylint: disable=unreachable - else: - # Initial body in streaming GZip response. - headers = MutableHeaders(raw=self.initial_message["headers"]) - headers["Content-Encoding"] = "gzip" - headers.add_vary_header("Accept-Encoding") - del headers["Content-Length"] - - self.gzip_file.write(body) - message["body"] = self.gzip_buffer.getvalue() - self.gzip_buffer.seek(0) - self.gzip_buffer.truncate() - - await self.send(self.initial_message) - await self.send(message) # pylint: disable=unreachable - - elif message_type == "http.response.body": - # Remaining body in streaming GZip response. - body = message.get("body", b"") - more_body = message.get("more_body", False) - - self.gzip_file.write(body) - if not more_body: - self.gzip_file.close() - - message["body"] = self.gzip_buffer.getvalue() - self.gzip_buffer.seek(0) - self.gzip_buffer.truncate() - - await self.send(message) - - else: - # Pass through other message types unmodified. - if not self.started: - self.started = True - await self.send(self.initial_message) - await self.send(message) + message_type = message["type"] + if message_type == "http.response.start": + await self._on_response_start(message) + elif message_type == "http.response.body": + await self._on_response_body(message) + else: + await self._pass_through(message)Add these helper methods inside
GZipResponder(belowsend_with_gzip):async def _on_response_start(self, message: Message) -> None: # Defer sending headers until we know if we will compress self.initial_message = message headers = Headers(raw=self.initial_message["headers"]) self.content_encoding_set = "content-encoding" in headers async def _on_response_body(self, message: Message) -> None: if self.content_encoding_set: if not self.started: self.started = True await self.send(self.initial_message) await self.send(message) return if not self.started: self.started = True body = message.get("body", b"") more_body = message.get("more_body", False) # Skip gzip for small bodies and for octet-streams (and other already-compressed types). init_headers = Headers(raw=self.initial_message["headers"]) content_type = (init_headers.get("content-type") or "").lower() is_uncompressible = ( content_type.startswith("application/octet-stream") or content_type.startswith("image/") or content_type.startswith("video/") ) if ((len(body) < self.minimum_size) and not more_body) or is_uncompressible: await self.send(self.initial_message) await self.send(message) return if not more_body: # Standard non-streaming gzip self.gzip_file.write(body) self.gzip_file.close() compressed = self.gzip_buffer.getvalue() headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Encoding"] = "gzip" headers["Content-Length"] = str(len(compressed)) headers.add_vary_header("Accept-Encoding") message["body"] = compressed await self.send(self.initial_message) await self.send(message) return # Streaming gzip: first chunk headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Encoding"] = "gzip" headers.add_vary_header("Accept-Encoding") if "Content-Length" in headers: del headers["Content-Length"] self.gzip_file.write(body) message["body"] = self.gzip_buffer.getvalue() self.gzip_buffer.seek(0) self.gzip_buffer.truncate() await self.send(self.initial_message) await self.send(message) return # Streaming gzip: subsequent chunks body = message.get("body", b"") more_body = message.get("more_body", False) self.gzip_file.write(body) if not more_body: self.gzip_file.close() message["body"] = self.gzip_buffer.getvalue() self.gzip_buffer.seek(0) self.gzip_buffer.truncate() await self.send(message) async def _pass_through(self, message: Message) -> None: # Pass through other message types unmodified. if not self.started: self.started = True await self.send(self.initial_message) await self.send(message)This should satisfy pylint and make the control flow easier to reason about.
🧹 Nitpick comments (8)
src/utils.py (8)
18-31: Consider using a dataclass and validate inputs in DiscretizeConfig.Current class is fine, but a dataclass reduces boilerplate and explicit validation prevents silent misconfigurations (negative samples, inverted bounds).
Example diff for minimal validation (keeps current API):
class DiscretizeConfig: @@ - def __init__( - self, bounds: Tuple[float, float] = (0, 10), samples: int = 200 - ): - self.bounds = bounds - self.samples = samples + def __init__( + self, bounds: Tuple[float, float] = (0, 10), samples: int = 200 + ): + lower, upper = bounds + if lower >= upper: + raise ValueError(f"bounds must be (lower < upper), got {bounds}") + if samples <= 0: + raise ValueError(f"samples must be > 0, got {samples}") + self.bounds = (float(lower), float(upper)) + self.samples = int(samples)
32-47: Preset bounds look domain-specific—please verify they match intended usage across Environment/Motor/Rocket/Flight.
- These presets will drive discretization resolution and can affect performance and accuracy.
- If they’re empirically chosen, consider documenting the rationale next to each preset.
Would you like a quick benchmark harness to compare encode time/size vs. samples for typical objects?
77-87: Preserve Function metadata and skip already-discrete functions.Reconstructing with
Function(attr_value.source)may drop metadata (name, interpolation, units). Prefer deepcopy; also skip ifis_discreteis already true.- if callable(attr_value) and isinstance(attr_value, Function): + if isinstance(attr_value, Function): try: - discretized_func = Function(attr_value.source) - discretized_func.set_discrete( + # Skip functions already discretized + if getattr(attr_value, "is_discrete", False): + continue + # Preserve metadata + discretized_func = copy.deepcopy(attr_value) + discretized_func.set_discrete( lower=config.bounds[0], upper=config.bounds[1], samples=config.samples, mutate_self=True, ) setattr(obj_copy, attr_name, discretized_func)
92-103: Nice: deep-copy + official RocketPyEncoder + post-processing.This should address prior mutation concerns and the earlier “date encoded as list” report. The JSON round-trip is okay for robustness, but if RocketPy provides a direct dict encoder, consider using that to avoid an extra serialize/parse.
104-114: Fallback loses types by coercing to str; preserve JSON-serializable structure when possible.Keep numbers/lists/dicts intact and only stringify non-serializable leaves.
- attributes[attr_name] = str(attr_value) + try: + # Preserve JSON-serializable types; stringify unknowns + attributes[attr_name] = json.loads( + json.dumps(attr_value, default=str) + ) + except Exception: + attributes[attr_name] = str(attr_value)
117-149: Type hints and edge cases in _fix_datetime_fields.
- Add type hints.
- Consider timezone support and month/day bounds if the encoder ever emits tz info or invalid lists. Current try/except is safe; documenting assumptions helps.
-def _fix_datetime_fields(data): +def _fix_datetime_fields(data: Any) -> Any:If tz-awareness becomes necessary later, we can extend this to detect an 8th/9th element for offset and attach a timezone.
217-221: Use Headers API for Content-Type instead of raw tuple scan.Raw header values may include parameters (e.g., charset) and casing varies. Use
Headers(...).get("content-type")and a prefix check. The larger refactor above includes this.
176-176: Attribution comment is helpful; consider pinning Starlette version assumptions.Since this is a fork of Starlette gzip, consider noting the Starlette version you mirrored to ease future diffs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/utils.py(3 hunks)
🧰 Additional context used
🪛 GitHub Actions: Pylint
src/utils.py
[error] 198-198: pylint: R0915 Too many statements (55/50) (too-many-statements)
🔇 Additional comments (4)
src/utils.py (4)
151-174: Middleware defaults LGTM.Sensible defaults for
minimum_sizeandcompresslevel, and clean Accept-Encoding gating.
65-65: Kudos for addressing the earlier mutation concern.Deep-copying the object prior to discretization aligns with prior review feedback and avoids shared-state bugs.
101-103: Confirm downstream expects datetime objects post-fix.
_fix_datetime_fieldsreturns Pythondatetimeobjects. If responses are later serialized by FastAPI/Pydantic, that’s fine (ISO strings). If they are consumed as plain dicts elsewhere, ensure those consumers handledatetimecorrectly.
1-114: No deprecated utilities or stray encoding calls detected
Repository-wide verification confirms:
- No references to
get_instance_attributes,to_python_primitive, orAnyToPrimitive.- No direct
json.dumps(..., cls=RocketPyEncoder)invocations.- No ad-hoc
setattr(..., Function(...))patterns outsiderocketpy_encoder.The new
rocketpy_encoderfully consolidates serialization logic. No further action required.
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.
Actionable comments posted: 2
♻️ Duplicate comments (4)
src/views/motor.py (1)
18-22: Good: preserves None-omission and enables dynamic attrsIncluding ser_json_exclude_none=True while allowing extras and arbitrary types keeps responses clean and supports encoder-driven fields. Matches the pattern used across views.
src/views/environment.py (2)
19-23: Good: merges parent behavior and supports dynamic attrsKeeping ser_json_exclude_none=True avoids null noise; extra='allow' and arbitrary_types_allowed=True are appropriate for encoder output.
44-46: Fix timezone-naive, import-time datetime defaultsDefaults evaluated at import time and naive datetimes lead to non-deterministic behavior across workers/timezones. Use Field(default_factory=...) with timezone-aware now().
Apply this diff:
-from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone -from pydantic import ConfigDict +from pydantic import ConfigDict, Field @@ - date: Optional[datetime] = datetime.today() + timedelta(days=1) - local_date: Optional[datetime] = datetime.today() + timedelta(days=1) - datetime_date: Optional[datetime] = datetime.today() + timedelta(days=1) + date: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc) + timedelta(days=1)) + local_date: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc) + timedelta(days=1)) + datetime_date: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc) + timedelta(days=1))src/views/rocket.py (1)
21-25: Good: consistent model_config with None-omissionMatches other simulations; keeps responses lean while allowing dynamic fields.
🧹 Nitpick comments (9)
src/views/motor.py (3)
26-26: Nit: “always present” is misleadingThese “core” fields are Optional and may be omitted by the encoder; consider rewording to “commonly present” to avoid confusing API consumers.
30-30: Tighten type for dry_inertia to match model and docsMotorModel defines dry_inertia as a 3-tuple of floats. Using plain tuple loses structure and harms schema quality.
Apply this diff:
- dry_inertia: Optional[tuple] = None + dry_inertia: Optional[tuple[float, float, float]] = None
48-75: Optional: introduce a shared alias for encoded seriesMany of these fields represent discretized series/curves. Define a reusable type alias (e.g., EncodedSeries = list[float] | list[tuple[float, float]] | dict[str, Any]) to improve generated docs and client expectations.
I can draft a small EncodedSeries alias and apply it across views if you want.
src/views/environment.py (3)
30-30: Default elevation=1 is surprising; prefer None or project defaultEnvironmentModel uses 0.0. Consider aligning or using None to omit it from JSON when not provided.
Apply this diff:
- elevation: Optional[float] = 1 + elevation: Optional[float] = None
43-43: Type of max_expected_height likely needs floatHeights are usually non-integer; int restricts precision.
Apply this diff:
- max_expected_height: Optional[int] = None + max_expected_height: Optional[float] = None
48-71: Optional: shared alias for encoded curves/fieldsSimilar to Motor/Rocket, these Optional[Any] series could benefit from a shared EncodedSeries alias to improve generated API docs without over-constraining shapes.
Happy to prepare the alias and update these fields if you want.
src/views/rocket.py (3)
42-60: Optional: consolidate function-attribute typing via aliasSame suggestion as Motor/Environment: define EncodedSeries to describe these discretized outputs, improving OpenAPI docs while keeping flexibility.
I can introduce EncodedSeries in a shared module and update these fields in one PR commit.
37-37: Unifycenter_of_mass_without_motortype between model and viewThe
RocketModel.center_of_mass_without_motoris currently declared as anint(src/models/rocket.py:22), whereas in the view it’sOptional[float](src/views/rocket.py:37). This inconsistency may lead to confusion for API consumers and code generators.Choose one of the following refactoring options:
- Prefer
floateverywhere (more physically accurate):
- Update the model declaration to
float(orOptional[float]ifNoneis valid).- Ensure the view uses the same signature.
- Or match the model’s current
inttype for consistency:
- Change the view to
Optional[int].Locations to update:
- src/models/rocket.py (line 22)
- src/views/rocket.py (line 37)
Example diffs for each approach:
- Floating-point everywhere
--- src/models/rocket.py - center_of_mass_without_motor: int + center_of_mass_without_motor: float --- src/views/rocket.py - center_of_mass_without_motor: Optional[float] = None + center_of_mass_without_motor: float
- Integer everywhere
--- src/views/rocket.py - center_of_mass_without_motor: Optional[float] = None + center_of_mass_without_motor: Optional[int] = NonePlease update both definitions to the chosen type so that clients and code generators see a consistent schema.
39-41: Refine theparachutesfield to use the existing Parachute modelThe
parachutes: Optional[list] = Nonedeclaration insrc/views/rocket.py(around lines 39–41) can be tightened to leverage the Pydantic model you already have.• File:
src/views/rocket.py, lines 39–41
Change:- parachutes: Optional[list] = None + from typing import List + from src.models.sub.aerosurfaces import Parachute + + parachutes: Optional[List[Parachute]] = NoneThis ensures each parachute entry conforms to your domain schema.
• The
motor: Optional[MotorSimulation] = Nonefield is already typed appropriately; just verify that your encoder’s output aligns with the nestedMotorSimulationstructure.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
src/views/environment.py(2 hunks)src/views/flight.py(1 hunks)src/views/motor.py(1 hunks)src/views/rocket.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/views/flight.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-15T15:12:21.314Z
Learnt from: GabrielBarberini
PR: RocketPy-Team/Infinity-API#38
File: lib/routes/motor.py:75-75
Timestamp: 2024-11-15T15:12:21.314Z
Learning: When modifying API route paths in `lib/routes/motor.py`, changing from `/rocketpy/{motor_id}` to `/{motor_id}/rocketpy` is acceptable when no external consumers are affected. It's acceptable to introduce this breaking change if the team has decided to adopt the new approach, provided that the `README` and related documentation are updated accordingly.
Applied to files:
src/views/motor.py
🧬 Code graph analysis (3)
src/views/environment.py (4)
src/views/interface.py (1)
ApiBaseView(4-6)src/services/environment.py (5)
environment(38-39)environment(42-43)EnvironmentService(11-68)from_env_model(18-39)__init__(14-15)src/models/environment.py (1)
EnvironmentModel(6-50)src/controllers/environment.py (1)
EnvironmentController(10-61)
src/views/rocket.py (5)
src/services/rocket.py (3)
rocket(96-97)rocket(100-101)get_rocket_simulation(103-112)src/models/rocket.py (2)
RocketModel(13-63)DELETED(45-46)src/views/interface.py (1)
ApiBaseView(4-6)src/views/motor.py (2)
MotorView(78-79)MotorSimulation(7-75)tests/unit/test_routes/test_rockets_route.py (1)
stub_rocket_simulation_dump(24-27)
src/views/motor.py (3)
src/views/interface.py (1)
ApiBaseView(4-6)src/services/motor.py (4)
motor(129-130)motor(133-134)MotorService(23-154)get_motor_simulation(136-145)src/models/motor.py (1)
MotorModel(16-95)
🔇 Additional comments (2)
src/views/rocket.py (2)
30-37: Nice: tightened types for inertia and drag curvesPrecise tuple and list[tuple[float,float]] types materially improve schema/doc quality.
8-19: No remaining references to get_instance_attributes foundThe ripgrep search (
rg -n -C2 -g '!**/dist/**' -g '!**/build/**' '\bget_instance_attributes\s*\(') returned no matches, confirming that all calls toget_instance_attributeshave been removed repo-wide and replaced with the newrocketpy_encoder. No further action is required here.
aasitvora99
left a comment
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.
send it 🚀
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.
Actionable comments posted: 2
♻️ Duplicate comments (5)
src/utils.py (2)
83-93: Copying Function viasourcemay drop metadata; prefer deepcopy or guarded fallbackCreating a new Function from
.sourcecan lose configured state (units, interpolation, metadata). Use deepcopy when possible, or guard whensourceis absent.Apply this diff:
- if callable(attr_value) and isinstance(attr_value, Function): + if callable(attr_value) and isinstance(attr_value, Function): try: - discretized_func = Function(attr_value.source) + # Try to preserve full state first + try: + discretized_func = copy.deepcopy(attr_value) + except Exception: + # Fallback: rebuild from source if available + if hasattr(attr_value, "source") and attr_value.source is not None: + discretized_func = Function(attr_value.source) + else: + raise discretized_func.set_discrete( lower=config.bounds[0], upper=config.bounds[1], samples=config.samples, mutate_self=True, )
8-8: Avoid constructed default argument; use Optional[DiscretizeConfig] = NoneThe function already normalizes None to DiscretizeConfig(), but the signature still constructs a default instance at import time. This can lead to subtle shared-state bugs and contradicts the intent. Fix the signature and import Optional.
Apply this diff:
-from typing import NoReturn, Tuple +from typing import NoReturn, Tuple, Optional @@ -def rocketpy_encoder(obj, config: DiscretizeConfig = DiscretizeConfig()): +def rocketpy_encoder(obj, config: Optional[DiscretizeConfig] = None): @@ - if config is None: - config = DiscretizeConfig() + if config is None: + config = DiscretizeConfig()Run to ensure no callers rely on positional default:
#!/bin/bash rg -n "rocketpy_encoder\(" -C2Also applies to: 49-67
src/views/motor.py (3)
28-28: dry_inertia typing is too generic; specify element typesUse a concrete 3-tuple of floats to match semantics and aid validation.
- dry_inertia: Optional[tuple] = None + dry_inertia: Optional[tuple[float, float, float]] = None
24-36: Make burn_time a scalar core attribute, not a function-like seriesRocketPy’s burn_time is a scalar duration. Modeling it as Optional[Any] under “Function attributes” weakens guarantees and can break clients expecting a float.
Apply these diffs:
- Add burn_time to the core section:
@@ # Core Motor attributes (always present) burn_start_time: Optional[float] = None burn_out_time: Optional[float] = None + burn_time: Optional[float] = None dry_mass: Optional[float] = None
- Remove the function-like variant:
- burn_time: Optional[Any] = NoneIf the encoder sometimes emits time series for edge cases, consider a separate field name (e.g., burn_time_series) to avoid overloading semantics.
Also applies to: 55-56
1-2: Rename interpolate → interpolation_method and keep backward compatibility with aliasEverywhere else (models, services, docs) uses interpolation_method. Keeping interpolate here creates drift and brittle client contracts. Recommend renaming and accepting the legacy key via Field(alias='interpolate').
Apply this diff:
-from typing import Optional, Any -from pydantic import ConfigDict +from typing import Optional, Any +from pydantic import ConfigDict, Field @@ - interpolate: Optional[str] = None + interpolation_method: Optional[str] = Field( + default=None, + alias="interpolate", + description="Interpolation method used for discretization (e.g., 'linear', 'spline')." + )Follow-up: If any downstream code reads .interpolate, update it to .interpolation_method. The alias ensures inbound JSON with "interpolate" still works during transition.
Also applies to: 41-41
🧹 Nitpick comments (11)
src/utils.py (2)
111-120: Fallback path coerces values to strings; preserve JSON-serializable primitivesCasting all non-callables to str can silently change types (e.g., numbers -> strings). Prefer preserving primitives and only repr() when not JSON-serializable.
Apply this diff:
- attributes = {} + attributes = {} for attr_name in dir(obj_copy): if not attr_name.startswith('_'): try: attr_value = getattr(obj_copy, attr_name) if not callable(attr_value): - attributes[attr_name] = str(attr_value) + try: + # Preserve as-is if JSON-serializable + json.dumps(attr_value) + attributes[attr_name] = attr_value + except TypeError: + attributes[attr_name] = repr(attr_value) except Exception: continue return attributes
99-106: Avoid JSON round-trip if the encoder exposes a dict interfaceIf RocketPyEncoder supports returning a dict (e.g., via a .default method or similar), bypassing dumps/loads will reduce overhead and avoid intermediate type coercions.
Would you like me to check the latest RocketPyEncoder API and propose a concrete change?
src/views/environment.py (3)
5-5: Deduplicate datetime factory by importing the shared helper from modelsThis file redefines _default_future_datetime already provided in src/models/environment.py. Importing the shared helper avoids drift.
Apply this diff:
-from src.models.environment import EnvironmentModel +from src.models.environment import EnvironmentModel, _default_future_datetime @@ -def _default_future_datetime() -> datetime: - """Factory function to create timezone-aware datetime one day in the future.""" - return datetime.now(timezone.utc) + timedelta(days=1) +# Use the shared factory imported from models to ensure consistent behaviorAlso applies to: 8-10
30-47: Align elevation default with model or make it explicit why it differsEnvironmentModel uses elevation: Optional[float] = 0.0, while EnvironmentSimulation sets elevation: Optional[float] = 1. This inconsistency may confuse clients and tests.
Apply this diff if 0.0 is the intended default:
- elevation: Optional[float] = 1 + elevation: Optional[float] = 0.0If 1 meter is intentional for simulations, add a brief comment explaining the rationale.
55-78: Very broad Any types for function attributes; consider tightening where knownWhere RocketPy’s encoder shape is stable (e.g., sequences of [x, y] pairs), tightening to concrete types (like list[tuple[float, float]]) improves validation and docs. Keep Any for truly dynamic fields.
If you share a few sample payloads, I can propose precise typings for the most common fields.
src/views/flight.py (2)
9-19: Be cautious with multiple inheritance of Pydantic modelsCombining RocketSimulation and EnvironmentSimulation inflates the schema and can create field collisions/MRO surprises. Composition (nested models) or a common base may be easier to reason about and version.
I can sketch a composed FlightSimulation with namespaced rocket/environment submodels if you want to explore that path.
32-35: Typeinitial_solutionconcretelyIf this represents an array of floats (typical for ODE initial conditions), specify list[float] for stronger validation.
Apply this diff:
- initial_solution: Optional[list] = None + initial_solution: Optional[list[float]] = Nonesrc/views/motor.py (4)
12-16: Comment consistency: “rocketpy_encoder” vs “RocketPy encoders”Minor naming drift in comments: you refer to the encoder as rocketpy_encoder in one place and RocketPyEncoder elsewhere. Pick one convention (the PR title says “official RocketPy encoders”).
@@ - # discretized by rocketpy_encoder - # serialized by RocketPyEncoder + # Discretized and serialized by RocketPy encodersAlso applies to: 43-46
39-40: Narrow these to validated enums/literals (optional)If these values are expected to belong to known sets, tighten typing to catch invalid payloads early and improve API docs.
Example diff (keeps flexibility but adds validation):
-from typing import Optional, Any +from typing import Optional, Any, Literal @@ - coordinate_system_orientation: Optional[str] = None - motor_kind: Optional[str] = None + coordinate_system_orientation: Optional[ + Literal['nozzle_to_combustion_chamber', 'combustion_chamber_to_nozzle'] + ] = None + motor_kind: Optional[str] = None # or import and use MotorKinds if encoder emits enum-safe valuesIf the encoder emits the exact MotorKinds values, you can also:
-from src.models.motor import MotorModel +from src.models.motor import MotorModel, MotorKinds @@ - motor_kind: Optional[str] = None + motor_kind: Optional[MotorKinds] = None
46-74: Replace broad Any types for series with a documented type alias (optional)Most “function attributes” look like discretized series or scalars. Using Any everywhere hurts discoverability and client generation.
Pattern proposal:
-from typing import Optional, Any +from typing import Optional, Any, Sequence, Tuple, Union, TypeAlias + +# Series of (time, value) pairs or a scalar value +EncodedSeries: TypeAlias = Sequence[Tuple[float, float]] +EncodedValue: TypeAlias = Union[float, EncodedSeries] @@ - burn_area: Optional[Any] = None - burn_rate: Optional[Any] = None + burn_area: Optional[EncodedValue] = None + burn_rate: Optional[EncodedValue] = None @@ - thrust: Optional[Any] = None + thrust: Optional[EncodedValue] = NoneI can push a batch edit to convert the remaining attributes in this block to EncodedValue; say the word and I’ll prepare a follow-up diff.
1-1: Minor import hygieneWith the above suggestions (Field, Literal, type aliases), keep imports consolidated and minimal. No action needed if you accept the diffs as-is.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
pyproject.toml(1 hunks)src/models/environment.py(2 hunks)src/utils.py(3 hunks)src/views/environment.py(2 hunks)src/views/flight.py(1 hunks)src/views/motor.py(1 hunks)src/views/rocket.py(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
- src/views/rocket.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-15T15:12:21.314Z
Learnt from: GabrielBarberini
PR: RocketPy-Team/Infinity-API#38
File: lib/routes/motor.py:75-75
Timestamp: 2024-11-15T15:12:21.314Z
Learning: When modifying API route paths in `lib/routes/motor.py`, changing from `/rocketpy/{motor_id}` to `/{motor_id}/rocketpy` is acceptable when no external consumers are affected. It's acceptable to introduce this breaking change if the team has decided to adopt the new approach, provided that the `README` and related documentation are updated accordingly.
Applied to files:
src/views/motor.py
🧬 Code graph analysis (4)
src/models/environment.py (2)
src/models/interface.py (1)
ApiBaseModel(10-61)src/views/environment.py (1)
_default_future_datetime(8-10)
src/views/environment.py (2)
src/views/interface.py (1)
ApiBaseView(4-6)src/models/environment.py (2)
EnvironmentModel(12-54)_default_future_datetime(7-9)
src/views/motor.py (3)
src/views/interface.py (1)
ApiBaseView(4-6)src/services/motor.py (3)
motor(129-130)motor(133-134)get_motor_simulation(136-145)src/models/motor.py (1)
MotorModel(16-95)
src/views/flight.py (4)
src/services/flight.py (3)
flight(44-45)flight(48-49)FlightService(14-69)src/models/flight.py (1)
FlightModel(7-71)src/views/interface.py (1)
ApiBaseView(4-6)src/views/rocket.py (2)
RocketView(62-64)RocketSimulation(8-59)
🔇 Additional comments (5)
src/models/environment.py (1)
7-9: Good move to timezone-aware, per-instance defaultsSwitching to Field(default_factory=...) with a UTC-aware factory eliminates import-time evaluation and naive datetimes. This aligns well with ApiBaseModel serialization behavior.
Also applies to: 29-29
src/views/environment.py (1)
24-26: LGTM on model_configIncluding ser_json_exclude_none while allowing extras keeps responses clean and future-proof.
src/views/flight.py (1)
21-23: LGTM on model_configser_json_exclude_none with extra='allow' is consistent with other views and prevents None spam in responses.
src/views/motor.py (2)
18-20: Model config aligns with repo conventions (exclude None + allow extras) — LGTMGood job mirroring the other Simulation views by enabling ser_json_exclude_none and extra='allow'. This keeps responses lean and future‑proof for dynamic encoder fields.
7-16: Nice: dynamic-attr friendly Simulation view with extra='allow'The docstring and extra='allow' approach is a good fit for the new encoder-driven payloads and buys us forward compatibility as RocketPy evolves.
| try: | ||
| json_str = json.dumps( | ||
| obj_copy, | ||
| cls=RocketPyEncoder, | ||
| include_outputs=True, | ||
| include_function_data=True, | ||
| ) | ||
| encoded_result = json.loads(json_str) | ||
|
|
||
| return str(v) | ||
| # Post-process to fix datetime fields that got converted to lists | ||
| return _fix_datetime_fields(encoded_result) | ||
| except Exception as e: |
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.
Naive datetimes reintroduced by post-processing; make them UTC-aware
_fix_datetime_fields builds naive datetime objects, conflicting with the new UTC-aware defaults elsewhere (e.g., Environment defaults). This inconsistency can break consumers expecting tz-aware timestamps.
Apply this diff:
-from datetime import datetime
+from datetime import datetime, timezone
@@
- fixed[key] = datetime(
- year, month, day, hour, minute, second, microsecond
- )
+ fixed[key] = datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=timezone.utc
+ )Also applies to: 123-155
🤖 Prompt for AI Agents
In src/utils.py around lines 98 to 109 (and also apply same change to lines
123-155), _fix_datetime_fields currently reconstructs datetime objects as naive
datetimes; update the post-processing so any datetime reconstructed from
lists/tuples is made timezone-aware by setting UTC (e.g., use datetime(...,
tzinfo=timezone.utc) or .replace(tzinfo=timezone.utc)) instead of leaving them
naive, ensuring all returned datetimes are UTC-aware and consistent with
Environment defaults; adjust any imports and tests as needed to use
datetime.timezone.utc.
| from typing import Optional, Any | ||
| from pydantic import ConfigDict | ||
| from src.views.interface import ApiBaseView | ||
| from src.models.motor import MotorModel | ||
| from src.utils import AnyToPrimitive | ||
|
|
||
|
|
||
| class MotorSimulation(BaseModel): | ||
| class MotorSimulation(ApiBaseView): | ||
| """ | ||
| Motor simulation view that handles dynamically | ||
| encoded RocketPy Motor attributes. | ||
| Uses the new rocketpy_encoder which may return | ||
| different attributes based on the actual RocketPy Motor object. | ||
| The model allows extra fields to accommodate any | ||
| new attributes that might be encoded. | ||
| """ | ||
|
|
||
| model_config = ConfigDict( | ||
| ser_json_exclude_none=True, extra='allow', arbitrary_types_allowed=True | ||
| ) | ||
|
|
||
| message: str = "Motor successfully simulated" | ||
| average_thrust: Optional[float] = None | ||
| burn_duration: Optional[float] = None | ||
| burn_out_time: Optional[float] = None | ||
|
|
||
| # Core Motor attributes (always present) | ||
| burn_start_time: Optional[float] = None | ||
| center_of_dry_mass_position: Optional[float] = None | ||
| coordinate_system_orientation: str = 'nozzle_to_combustion_chamber' | ||
| dry_I_11: Optional[float] = None | ||
| dry_I_12: Optional[float] = None | ||
| dry_I_13: Optional[float] = None | ||
| dry_I_22: Optional[float] = None | ||
| dry_I_23: Optional[float] = None | ||
| dry_I_33: Optional[float] = None | ||
| burn_out_time: Optional[float] = None | ||
| dry_mass: Optional[float] = None | ||
| grain_burn_out: Optional[float] = None | ||
| grain_density: Optional[float] = None | ||
| grain_initial_height: Optional[float] = None | ||
| grain_initial_inner_radius: Optional[float] = None | ||
| grain_initial_mass: Optional[float] = None | ||
| grain_initial_volume: Optional[float] = None | ||
| dry_inertia: Optional[tuple] = None | ||
| center_of_dry_mass_position: Optional[float] = None | ||
| grains_center_of_mass_position: Optional[float] = None | ||
| grain_number: Optional[int] = None | ||
| grain_density: Optional[float] = None | ||
| grain_outer_radius: Optional[float] = None | ||
| grain_separation: Optional[float] = None | ||
| grains_center_of_mass_position: Optional[float] = None | ||
| interpolate: Optional[str] = None | ||
| max_thrust: Optional[float] = None | ||
| max_thrust_time: Optional[float] = None | ||
| nozzle_position: Optional[float] = None | ||
| grain_initial_inner_radius: Optional[float] = None | ||
| grain_initial_height: Optional[float] = None | ||
| nozzle_radius: Optional[float] = None | ||
| propellant_initial_mass: Optional[float] = None | ||
| throat_area: Optional[float] = None | ||
| throat_radius: Optional[float] = None | ||
| thrust_source: Optional[List[List[float]]] = None | ||
| total_impulse: Optional[float] = None | ||
| Kn: Optional[AnyToPrimitive] = None | ||
| I_11: Optional[AnyToPrimitive] = None | ||
| I_12: Optional[AnyToPrimitive] = None | ||
| I_13: Optional[AnyToPrimitive] = None | ||
| I_22: Optional[AnyToPrimitive] = None | ||
| I_23: Optional[AnyToPrimitive] = None | ||
| I_33: Optional[AnyToPrimitive] = None | ||
| burn_area: Optional[AnyToPrimitive] = None | ||
| burn_rate: Optional[AnyToPrimitive] = None | ||
| burn_time: Optional[AnyToPrimitive] = None | ||
| center_of_mass: Optional[AnyToPrimitive] = None | ||
| center_of_propellant_mass: Optional[AnyToPrimitive] = None | ||
| exhaust_velocity: Optional[AnyToPrimitive] = None | ||
| grain_height: Optional[AnyToPrimitive] = None | ||
| grain_volume: Optional[AnyToPrimitive] = None | ||
| grain_inner_radius: Optional[AnyToPrimitive] = None | ||
| mass_flow_rate: Optional[AnyToPrimitive] = None | ||
| propellant_I_11: Optional[AnyToPrimitive] = None | ||
| propellant_I_12: Optional[AnyToPrimitive] = None | ||
| propellant_I_13: Optional[AnyToPrimitive] = None | ||
| propellant_I_22: Optional[AnyToPrimitive] = None | ||
| propellant_I_23: Optional[AnyToPrimitive] = None | ||
| propellant_I_33: Optional[AnyToPrimitive] = None | ||
| propellant_mass: Optional[AnyToPrimitive] = None | ||
| reshape_thrust_curve: Optional[AnyToPrimitive] = None | ||
| total_mass: Optional[AnyToPrimitive] = None | ||
| total_mass_flow_rate: Optional[AnyToPrimitive] = None | ||
| thrust: Optional[AnyToPrimitive] = None | ||
| nozzle_position: Optional[float] = None | ||
| coordinate_system_orientation: Optional[str] = None | ||
| motor_kind: Optional[str] = None | ||
| interpolate: Optional[str] = None | ||
|
|
||
| # Function attributes | ||
| # discretized by rocketpy_encoder | ||
| # serialized by RocketPyEncoder | ||
| Kn: Optional[Any] = None | ||
| I_11: Optional[Any] = None | ||
| I_12: Optional[Any] = None | ||
| I_13: Optional[Any] = None | ||
| I_22: Optional[Any] = None | ||
| I_23: Optional[Any] = None | ||
| I_33: Optional[Any] = None | ||
| burn_area: Optional[Any] = None | ||
| burn_rate: Optional[Any] = None | ||
| burn_time: Optional[Any] = None | ||
| center_of_mass: Optional[Any] = None | ||
| center_of_propellant_mass: Optional[Any] = None | ||
| exhaust_velocity: Optional[Any] = None | ||
| grain_height: Optional[Any] = None | ||
| grain_volume: Optional[Any] = None | ||
| grain_inner_radius: Optional[Any] = None | ||
| mass_flow_rate: Optional[Any] = None | ||
| propellant_I_11: Optional[Any] = None | ||
| propellant_I_12: Optional[Any] = None | ||
| propellant_I_13: Optional[Any] = None | ||
| propellant_I_22: Optional[Any] = None | ||
| propellant_I_23: Optional[Any] = None | ||
| propellant_I_33: Optional[Any] = None | ||
| propellant_mass: Optional[Any] = None | ||
| reshape_thrust_curve: Optional[Any] = None | ||
| total_mass: Optional[Any] = None | ||
| total_mass_flow_rate: Optional[Any] = None | ||
| thrust: Optional[Any] = 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.
💡 Verification agent
🧩 Analysis chain
Sanity-check the PR objectives across the repo (encoder usage, naming, types)
To ensure we fully swapped get_instance_attributes → rocketpy_encoder and avoided naming/type drift, please run this verification script. Expectation: no remaining get_instance_attributes, uniform interpolation_method, and no function-like burn_time in views.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) Remaining usage of get_instance_attributes (should be none):"
rg -n -C1 "\bget_instance_attributes\b" || true
echo
echo "2) Check interpolation naming drift (prefer 'interpolation_method'):"
rg -n -C2 -g '!**/dist/**' -g '!**/build/**' '\binterpolat(e|ion_method)\b' || true
echo
echo "3) Views declaring burn_time as non-scalar (should be scalar float):"
rg -n -C2 -g 'src/views/**' '\bburn_time\s*:\s*Optional\[' || true
echo
echo "4) Grep for MotorSimulation schema dumps to see what clients receive:"
rg -n -C2 'MotorSimulation|get_motor_simulation' || trueLength of output: 11206
Refactor MotorSimulation view fields: rename interpolate to interpolation_method and drop the function-like burn_time
To align src/views/motor.py with the rest of the codebase and avoid API drift, please apply the following changes in the MotorSimulation class:
• Replace the incorrectly named interpolation field:
- interpolate: Optional[str] = None
+ interpolation_method: Optional[str] = None• Remove the function-attribute declaration for burn_time (it conflicts with the scalar burn_start_time/burn_out_time API):
- burn_time: Optional[Any] = None• After making these updates, verify that no other view subclasses (e.g., in src/views/rocket.py) explicitly declare interpolate or burn_time.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| from typing import Optional, Any | |
| from pydantic import ConfigDict | |
| from src.views.interface import ApiBaseView | |
| from src.models.motor import MotorModel | |
| from src.utils import AnyToPrimitive | |
| class MotorSimulation(BaseModel): | |
| class MotorSimulation(ApiBaseView): | |
| """ | |
| Motor simulation view that handles dynamically | |
| encoded RocketPy Motor attributes. | |
| Uses the new rocketpy_encoder which may return | |
| different attributes based on the actual RocketPy Motor object. | |
| The model allows extra fields to accommodate any | |
| new attributes that might be encoded. | |
| """ | |
| model_config = ConfigDict( | |
| ser_json_exclude_none=True, extra='allow', arbitrary_types_allowed=True | |
| ) | |
| message: str = "Motor successfully simulated" | |
| average_thrust: Optional[float] = None | |
| burn_duration: Optional[float] = None | |
| burn_out_time: Optional[float] = None | |
| # Core Motor attributes (always present) | |
| burn_start_time: Optional[float] = None | |
| center_of_dry_mass_position: Optional[float] = None | |
| coordinate_system_orientation: str = 'nozzle_to_combustion_chamber' | |
| dry_I_11: Optional[float] = None | |
| dry_I_12: Optional[float] = None | |
| dry_I_13: Optional[float] = None | |
| dry_I_22: Optional[float] = None | |
| dry_I_23: Optional[float] = None | |
| dry_I_33: Optional[float] = None | |
| burn_out_time: Optional[float] = None | |
| dry_mass: Optional[float] = None | |
| grain_burn_out: Optional[float] = None | |
| grain_density: Optional[float] = None | |
| grain_initial_height: Optional[float] = None | |
| grain_initial_inner_radius: Optional[float] = None | |
| grain_initial_mass: Optional[float] = None | |
| grain_initial_volume: Optional[float] = None | |
| dry_inertia: Optional[tuple] = None | |
| center_of_dry_mass_position: Optional[float] = None | |
| grains_center_of_mass_position: Optional[float] = None | |
| grain_number: Optional[int] = None | |
| grain_density: Optional[float] = None | |
| grain_outer_radius: Optional[float] = None | |
| grain_separation: Optional[float] = None | |
| grains_center_of_mass_position: Optional[float] = None | |
| interpolate: Optional[str] = None | |
| max_thrust: Optional[float] = None | |
| max_thrust_time: Optional[float] = None | |
| nozzle_position: Optional[float] = None | |
| grain_initial_inner_radius: Optional[float] = None | |
| grain_initial_height: Optional[float] = None | |
| nozzle_radius: Optional[float] = None | |
| propellant_initial_mass: Optional[float] = None | |
| throat_area: Optional[float] = None | |
| throat_radius: Optional[float] = None | |
| thrust_source: Optional[List[List[float]]] = None | |
| total_impulse: Optional[float] = None | |
| Kn: Optional[AnyToPrimitive] = None | |
| I_11: Optional[AnyToPrimitive] = None | |
| I_12: Optional[AnyToPrimitive] = None | |
| I_13: Optional[AnyToPrimitive] = None | |
| I_22: Optional[AnyToPrimitive] = None | |
| I_23: Optional[AnyToPrimitive] = None | |
| I_33: Optional[AnyToPrimitive] = None | |
| burn_area: Optional[AnyToPrimitive] = None | |
| burn_rate: Optional[AnyToPrimitive] = None | |
| burn_time: Optional[AnyToPrimitive] = None | |
| center_of_mass: Optional[AnyToPrimitive] = None | |
| center_of_propellant_mass: Optional[AnyToPrimitive] = None | |
| exhaust_velocity: Optional[AnyToPrimitive] = None | |
| grain_height: Optional[AnyToPrimitive] = None | |
| grain_volume: Optional[AnyToPrimitive] = None | |
| grain_inner_radius: Optional[AnyToPrimitive] = None | |
| mass_flow_rate: Optional[AnyToPrimitive] = None | |
| propellant_I_11: Optional[AnyToPrimitive] = None | |
| propellant_I_12: Optional[AnyToPrimitive] = None | |
| propellant_I_13: Optional[AnyToPrimitive] = None | |
| propellant_I_22: Optional[AnyToPrimitive] = None | |
| propellant_I_23: Optional[AnyToPrimitive] = None | |
| propellant_I_33: Optional[AnyToPrimitive] = None | |
| propellant_mass: Optional[AnyToPrimitive] = None | |
| reshape_thrust_curve: Optional[AnyToPrimitive] = None | |
| total_mass: Optional[AnyToPrimitive] = None | |
| total_mass_flow_rate: Optional[AnyToPrimitive] = None | |
| thrust: Optional[AnyToPrimitive] = None | |
| nozzle_position: Optional[float] = None | |
| coordinate_system_orientation: Optional[str] = None | |
| motor_kind: Optional[str] = None | |
| interpolate: Optional[str] = None | |
| # Function attributes | |
| # discretized by rocketpy_encoder | |
| # serialized by RocketPyEncoder | |
| Kn: Optional[Any] = None | |
| I_11: Optional[Any] = None | |
| I_12: Optional[Any] = None | |
| I_13: Optional[Any] = None | |
| I_22: Optional[Any] = None | |
| I_23: Optional[Any] = None | |
| I_33: Optional[Any] = None | |
| burn_area: Optional[Any] = None | |
| burn_rate: Optional[Any] = None | |
| burn_time: Optional[Any] = None | |
| center_of_mass: Optional[Any] = None | |
| center_of_propellant_mass: Optional[Any] = None | |
| exhaust_velocity: Optional[Any] = None | |
| grain_height: Optional[Any] = None | |
| grain_volume: Optional[Any] = None | |
| grain_inner_radius: Optional[Any] = None | |
| mass_flow_rate: Optional[Any] = None | |
| propellant_I_11: Optional[Any] = None | |
| propellant_I_12: Optional[Any] = None | |
| propellant_I_13: Optional[Any] = None | |
| propellant_I_22: Optional[Any] = None | |
| propellant_I_23: Optional[Any] = None | |
| propellant_I_33: Optional[Any] = None | |
| propellant_mass: Optional[Any] = None | |
| reshape_thrust_curve: Optional[Any] = None | |
| total_mass: Optional[Any] = None | |
| total_mass_flow_rate: Optional[Any] = None | |
| thrust: Optional[Any] = None | |
| ++ b/src/views/motor.py | |
| @@ class MotorSimulation(ApiBaseView): | |
| # Core Motor attributes (always present) | |
| burn_start_time: Optional[float] = None | |
| burn_out_time: Optional[float] = None | |
| dry_mass: Optional[float] = None | |
| dry_inertia: Optional[tuple] = None | |
| center_of_dry_mass_position: Optional[float] = None | |
| grains_center_of_mass_position: Optional[float] = None | |
| grain_number: Optional[int] = None | |
| grain_density: Optional[float] = None | |
| grain_outer_radius: Optional[float] = None | |
| grain_initial_inner_radius: Optional[float] = None | |
| grain_initial_height: Optional[float] = None | |
| nozzle_radius: Optional[float] = None | |
| throat_radius: Optional[float] = None | |
| nozzle_position: Optional[float] = None | |
| coordinate_system_orientation: Optional[str] = None | |
| motor_kind: Optional[str] = None | |
| interpolation_method: Optional[str] = None | |
| # Function attributes | |
| # discretized by rocketpy_encoder | |
| # serialized by RocketPyEncoder | |
| Kn: Optional[Any] = None | |
| I_11: Optional[Any] = None | |
| I_12: Optional[Any] = None | |
| I_13: Optional[Any] = None | |
| I_22: Optional[Any] = None | |
| I_23: Optional[Any] = None | |
| I_33: Optional[Any] = None | |
| burn_area: Optional[Any] = None | |
| burn_rate: Optional[Any] = None | |
| - burn_time: Optional[Any] = None | |
| center_of_mass: Optional[Any] = None | |
| center_of_propellant_mass: Optional[Any] = None | |
| exhaust_velocity: Optional[Any] = None | |
| grain_height: Optional[Any] = None | |
| grain_volume: Optional[Any] = None | |
| grain_inner_radius: Optional[Any] = None | |
| mass_flow_rate: Optional[Any] = None | |
| propellant_I_11: Optional[Any] = None | |
| propellant_I_12: Optional[Any] = None | |
| propellant_I_13: Optional[Any] = None | |
| propellant_I_22: Optional[Any] = None | |
| propellant_I_23: Optional[Any] = None | |
| propellant_I_33: Optional[Any] = None | |
| propellant_mass: Optional[Any] = None | |
| reshape_thrust_curve: Optional[Any] = None | |
| total_mass: Optional[Any] = None | |
| total_mass_flow_rate: Optional[Any] = None | |
| thrust: Optional[Any] = None |
🤖 Prompt for AI Agents
In src/views/motor.py lines 1-74: the MotorSimulation view declares a field
named interpolate (should be interpolation_method) and also declares a
function-like field burn_time which conflicts with scalar
burn_start_time/burn_out_time; rename the attribute interpolate to
interpolation_method (keep type Optional[str] and default None) and remove the
burn_time function-attribute entirely (delete its Optional[Any] declaration),
then run a quick grep across view subclasses (e.g., src/views/rocket.py and
other files under src/views) to ensure no other classes still declare
interpolate or a burn_time function field and update them similarly if found.
Pull Request Overview
This PR shifts the simulation encoding approach to utilize the official RocketPy encoders and a new DiscretizeConfig for improved dynamic attribute support across simulation models. Key changes include:
Summary by CodeRabbit
New Features
Refactor
Chores
Documentation