)]}'
{"/PATCHSET_LEVEL":[{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"6c828a4c1f34677dc88b968f65f2e70e6262b077","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":1,"id":"24bd9ba6_0cb225f5","updated":"2025-09-01 11:05:31.000000000","message":"patch rebased","commit_id":"66081311d5805b5828a4c97f340a34b10572e62d"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"ef6ae59ba2abdd38cb9e40b528d6aa89e56cdd83","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":1,"id":"b5bed2c5_b06df4c0","updated":"2025-06-30 23:01:12.000000000","message":"recheck","commit_id":"66081311d5805b5828a4c97f340a34b10572e62d"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"48068dc92ae30b7db8f895ccbafc68630258af42","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":1,"id":"dfc3cb93_033d8672","updated":"2025-07-02 16:49:00.000000000","message":"recheck","commit_id":"66081311d5805b5828a4c97f340a34b10572e62d"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"d0728ee6cc593d35650d47fd54af44414cfed346","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":3,"id":"f13c6e8e_915dceae","updated":"2025-09-16 12:41:00.000000000","message":"patch rebased","commit_id":"63e1868f7e6c01fb12e88af21386ad0b38fe25f8"},{"author":{"_account_id":34975,"name":"Jaromír Wysoglad","email":"jwysogla@redhat.com","username":"jwysogla"},"change_message_id":"c522085fdbde27c7d1f1fb77a03f359912652a0b","unresolved":true,"context_lines":[],"source_content_type":"","patch_set":4,"id":"e311ebf6_ab2f78c5","updated":"2025-11-05 19:28:53.000000000","message":"Also I\u0027m not sure what\u0027s CloudKitty\u0027s stance on releasenotes (I know in telemetry we often don\u0027t write releasenotes even though we probably should). But maybe a new feature like this should have one?","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":4264,"name":"Matthias Runge","email":"mrunge@redhat.com","username":"mrunge"},"change_message_id":"d06ca2138cd5d2811906038b03cabbef11796bab","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":4,"id":"b1c72f73_a69ee029","updated":"2025-09-22 07:42:16.000000000","message":"I missed that comma in my previous review","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"4c24f6956216e22a89c44b9d32b7ba9358fe562d","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":4,"id":"cac0f767_c6560f97","updated":"2025-09-16 16:25:56.000000000","message":"recheck","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"e526ba785b9b533c2b029d1735db9f0e73a38cc0","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":4,"id":"7ec78824_cd41d007","in_reply_to":"e311ebf6_ab2f78c5","updated":"2025-11-25 23:04:06.000000000","message":"Done","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"6bf8df32ff19eef3124d2e2a2426f59937838a74","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"4281f410_8e930f68","updated":"2025-12-13 00:18:03.000000000","message":"recheck oslo_issue","commit_id":"a2780db79a9f9ab21d7401525d6f0ed996015f95"},{"author":{"_account_id":35263,"name":"Matt Crees","email":"mattc@stackhpc.com","username":"mattcrees"},"change_message_id":"9a685b8ec1242d37891652811e120a9c6aa92a97","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":8,"id":"f528ee93_335253cc","updated":"2026-02-04 09:42:53.000000000","message":"Given the eval is locked behind an operator changing from default config, I think this is ok to merge.","commit_id":"82a768450ccc6c1bba2f34f8054c005639f43b69"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"d157ce3adc1f3b18e075ed280fe418067d487d97","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":8,"id":"a6fa57bf_571812b1","updated":"2026-01-29 20:54:41.000000000","message":"Hello guys, can we merge this one?","commit_id":"82a768450ccc6c1bba2f34f8054c005639f43b69"},{"author":{"_account_id":32968,"name":"Juan Larriba","email":"jlarriba@redhat.com","username":"jlarriba"},"change_message_id":"38558b6d7a3b0a58b508b193fcde58303ec4c042","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":8,"id":"0776a852_1c4c3d63","updated":"2026-01-07 12:19:08.000000000","message":"We have started to see this behaviour of storing 0\u0027s in the dataframes storage, which should not be there, so I am huge in favour of this to be introduced, even as a default config.","commit_id":"82a768450ccc6c1bba2f34f8054c005639f43b69"},{"author":{"_account_id":4264,"name":"Matthias Runge","email":"mrunge@redhat.com","username":"mrunge"},"change_message_id":"6879600a33ca4eb4fb3b33bccd2ccc4bd9416f87","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":8,"id":"2aadb033_eb394c04","updated":"2026-02-16 15:33:19.000000000","message":"recheck no_such_group oslo policy","commit_id":"82a768450ccc6c1bba2f34f8054c005639f43b69"}],"cloudkitty/orchestrator.py":[{"author":{"_account_id":15197,"name":"Pierre Riteau","email":"pierre@stackhpc.com","username":"priteau","status":"StackHPC"},"change_message_id":"78acd79942f5f75925843e0566e21b853e2546d2","unresolved":true,"context_lines":[{"line_number":526,"context_line":"        LOG.debug(\"Evaluating skip_datapoint_expression[%s] for datapoint \""},{"line_number":527,"context_line":"                  \"[%s] under metric [%s].\", skip_datapoint_expression,"},{"line_number":528,"context_line":"                  datapoint, usage_data_metric_name)"},{"line_number":529,"context_line":""},{"line_number":530,"context_line":"        if not eval(skip_datapoint_expression):"},{"line_number":531,"context_line":"            filtered_datapoints.append(datapoint)"},{"line_number":532,"context_line":"        else:"},{"line_number":533,"context_line":"            LOG.debug(\"Datapoint [%s] will be skipped because of \""}],"source_content_type":"text/x-python","patch_set":1,"id":"afca3d33_c6768bef","line":530,"range":{"start_line":529,"start_character":0,"end_line":530,"end_character":47},"updated":"2025-08-18 17:53:55.000000000","message":"I am not too keen on using eval() on input provided externally. I know this is admin-controlled so presumably it is safe, but it still provides one extra way to execute arbitrary code.\n\nDo we really need a custom expression or could be match against a provided value (maybe a float?)","commit_id":"66081311d5805b5828a4c97f340a34b10572e62d"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"99ae6be4872a0ba84ee09b57b2a3a0d71ab9b6ef","unresolved":false,"context_lines":[{"line_number":526,"context_line":"        LOG.debug(\"Evaluating skip_datapoint_expression[%s] for datapoint \""},{"line_number":527,"context_line":"                  \"[%s] under metric [%s].\", skip_datapoint_expression,"},{"line_number":528,"context_line":"                  datapoint, usage_data_metric_name)"},{"line_number":529,"context_line":""},{"line_number":530,"context_line":"        if not eval(skip_datapoint_expression):"},{"line_number":531,"context_line":"            filtered_datapoints.append(datapoint)"},{"line_number":532,"context_line":"        else:"},{"line_number":533,"context_line":"            LOG.debug(\"Datapoint [%s] will be skipped because of \""}],"source_content_type":"text/x-python","patch_set":1,"id":"4ff03243_52da8892","line":530,"range":{"start_line":529,"start_character":0,"end_line":530,"end_character":47},"in_reply_to":"11ef666d_c2fd632b","updated":"2025-09-01 11:05:29.000000000","message":"Done","commit_id":"66081311d5805b5828a4c97f340a34b10572e62d"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"9f2acbc0baf82cf4dc864746cd4674c6a6cbaf15","unresolved":true,"context_lines":[{"line_number":526,"context_line":"        LOG.debug(\"Evaluating skip_datapoint_expression[%s] for datapoint \""},{"line_number":527,"context_line":"                  \"[%s] under metric [%s].\", skip_datapoint_expression,"},{"line_number":528,"context_line":"                  datapoint, usage_data_metric_name)"},{"line_number":529,"context_line":""},{"line_number":530,"context_line":"        if not eval(skip_datapoint_expression):"},{"line_number":531,"context_line":"            filtered_datapoints.append(datapoint)"},{"line_number":532,"context_line":"        else:"},{"line_number":533,"context_line":"            LOG.debug(\"Datapoint [%s] will be skipped because of \""}],"source_content_type":"text/x-python","patch_set":1,"id":"11ef666d_c2fd632b","line":530,"range":{"start_line":529,"start_character":0,"end_line":530,"end_character":47},"in_reply_to":"afca3d33_c6768bef","updated":"2025-08-18 17:57:35.000000000","message":"The expression allows us to execute the skip based on whatever fits the operator needs. We use similar approach in Ceilometer dynamic pollsters, for instance.","commit_id":"66081311d5805b5828a4c97f340a34b10572e62d"},{"author":{"_account_id":34975,"name":"Jaromír Wysoglad","email":"jwysogla@redhat.com","username":"jwysogla"},"change_message_id":"c522085fdbde27c7d1f1fb77a03f359912652a0b","unresolved":true,"context_lines":[{"line_number":85,"context_line":"                    \u0027expression in a variable called \"datapoint\". The \u0027"},{"line_number":86,"context_line":"                    \u0027expression MUST return a True or False value. For \u0027"},{"line_number":87,"context_line":"                    \u0027instance, if one wants to skip persisting processed \u0027"},{"line_number":88,"context_line":"                    \u0027datapoints that hae QTY as zero, the following \u0027"},{"line_number":89,"context_line":"                    \u0027expression can be used: \u0027"},{"line_number":90,"context_line":"                    \u0027\"datapoint.get(\\\"vol\\\", {}).get(\\\"qty\\\", 0) \u003d\u003d 0\".\u0027"},{"line_number":91,"context_line":"               )"}],"source_content_type":"text/x-python","patch_set":4,"id":"2bbcc78b_5e85466b","line":88,"updated":"2025-11-05 19:28:53.000000000","message":"```suggestion\n                    \u0027datapoints that have QTY as zero, the following \u0027\n```","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"e526ba785b9b533c2b029d1735db9f0e73a38cc0","unresolved":false,"context_lines":[{"line_number":85,"context_line":"                    \u0027expression in a variable called \"datapoint\". The \u0027"},{"line_number":86,"context_line":"                    \u0027expression MUST return a True or False value. For \u0027"},{"line_number":87,"context_line":"                    \u0027instance, if one wants to skip persisting processed \u0027"},{"line_number":88,"context_line":"                    \u0027datapoints that hae QTY as zero, the following \u0027"},{"line_number":89,"context_line":"                    \u0027expression can be used: \u0027"},{"line_number":90,"context_line":"                    \u0027\"datapoint.get(\\\"vol\\\", {}).get(\\\"qty\\\", 0) \u003d\u003d 0\".\u0027"},{"line_number":91,"context_line":"               )"}],"source_content_type":"text/x-python","patch_set":4,"id":"275bfdb1_598b2f79","line":88,"in_reply_to":"2bbcc78b_5e85466b","updated":"2025-11-25 23:04:06.000000000","message":"Done","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":34975,"name":"Jaromír Wysoglad","email":"jwysogla@redhat.com","username":"jwysogla"},"change_message_id":"c522085fdbde27c7d1f1fb77a03f359912652a0b","unresolved":true,"context_lines":[{"line_number":87,"context_line":"                    \u0027instance, if one wants to skip persisting processed \u0027"},{"line_number":88,"context_line":"                    \u0027datapoints that hae QTY as zero, the following \u0027"},{"line_number":89,"context_line":"                    \u0027expression can be used: \u0027"},{"line_number":90,"context_line":"                    \u0027\"datapoint.get(\\\"vol\\\", {}).get(\\\"qty\\\", 0) \u003d\u003d 0\".\u0027"},{"line_number":91,"context_line":"               )"},{"line_number":92,"context_line":"]"},{"line_number":93,"context_line":""}],"source_content_type":"text/x-python","patch_set":4,"id":"35083948_f3489a8e","line":90,"updated":"2025-11-05 19:28:53.000000000","message":"This seems to give a lot of freedom on expressions I could define. But as someone not that much familiar with the CloudKitty code and wanting to use this new parameter, I wouldn\u0027t know how. I see how to skip processing based on \u0027qty\u0027 - so that\u0027s easy. But I as a user would surely want to know what else I can supply into the expression. What other values can I use instead of \u0027vol\u0027, what other values can I use instead of \u0027qty\u0027. I think it would be great to have some further documentation on this or at least a list of keys of each of the dictionaries.","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"e526ba785b9b533c2b029d1735db9f0e73a38cc0","unresolved":false,"context_lines":[{"line_number":87,"context_line":"                    \u0027instance, if one wants to skip persisting processed \u0027"},{"line_number":88,"context_line":"                    \u0027datapoints that hae QTY as zero, the following \u0027"},{"line_number":89,"context_line":"                    \u0027expression can be used: \u0027"},{"line_number":90,"context_line":"                    \u0027\"datapoint.get(\\\"vol\\\", {}).get(\\\"qty\\\", 0) \u003d\u003d 0\".\u0027"},{"line_number":91,"context_line":"               )"},{"line_number":92,"context_line":"]"},{"line_number":93,"context_line":""}],"source_content_type":"text/x-python","patch_set":4,"id":"85c71f47_d9345c1f","line":90,"in_reply_to":"35083948_f3489a8e","updated":"2025-11-25 23:04:06.000000000","message":"Documentation added. What do you think about it?","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":34975,"name":"Jaromír Wysoglad","email":"jwysogla@redhat.com","username":"jwysogla"},"change_message_id":"23551a4325bdf993e4ec2bbd64d2da367709ee05","unresolved":false,"context_lines":[{"line_number":87,"context_line":"                    \u0027instance, if one wants to skip persisting processed \u0027"},{"line_number":88,"context_line":"                    \u0027datapoints that hae QTY as zero, the following \u0027"},{"line_number":89,"context_line":"                    \u0027expression can be used: \u0027"},{"line_number":90,"context_line":"                    \u0027\"datapoint.get(\\\"vol\\\", {}).get(\\\"qty\\\", 0) \u003d\u003d 0\".\u0027"},{"line_number":91,"context_line":"               )"},{"line_number":92,"context_line":"]"},{"line_number":93,"context_line":""}],"source_content_type":"text/x-python","patch_set":4,"id":"812a77a1_6308e53c","line":90,"in_reply_to":"85c71f47_d9345c1f","updated":"2025-12-02 18:30:16.000000000","message":"It\u0027s still quite complex to imagine how the datapoint object looks like. But after reading it a few times, I think I understand how it\u0027d be used. And I don\u0027t have any suggestions, so the documentation looks good to me.","commit_id":"1885d13989584b53a061e3de2f573714eb06df77"},{"author":{"_account_id":32968,"name":"Juan Larriba","email":"jlarriba@redhat.com","username":"jlarriba"},"change_message_id":"d96b979344ad75da3ade120726625e9a6ae98897","unresolved":true,"context_lines":[{"line_number":524,"context_line":"                  \"[%s] under metric [%s].\", skip_datapoint_expression,"},{"line_number":525,"context_line":"                  datapoint, usage_data_metric_name)"},{"line_number":526,"context_line":""},{"line_number":527,"context_line":"        if not eval(skip_datapoint_expression):"},{"line_number":528,"context_line":"            filtered_datapoints.append(datapoint)"},{"line_number":529,"context_line":"        else:"},{"line_number":530,"context_line":"            LOG.debug(\"Datapoint [%s] will be skipped because of \""}],"source_content_type":"text/x-python","patch_set":5,"id":"303ebe06_b1465372","line":527,"updated":"2025-12-12 11:05:58.000000000","message":"I am concerned about this eval here. I might be just paranoid, but this is executing without filters anything the user configures in the skill_datapoint_expression, which seems quite prone to failure. I concur with Jaromir that for a user it might be quite difficult to know how a \"good\" expression has to look like, and that can derive into multiple issues that the user has not the appropiate tools to debug.\n\nI would suggest to replace eval() by something that is more limited, but easier to manage, some kind of predefined filter functions, something like skip_datapoints_filter \u003d zero_qty or skip_datapoints_filter \u003d qty_less_than:0.01","commit_id":"4746a09d8552d51170e1a1dc51db9f14bc33fef0"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"65faf9a55b5e36409003c2ccaee5c7dd0d3f5553","unresolved":true,"context_lines":[{"line_number":524,"context_line":"                  \"[%s] under metric [%s].\", skip_datapoint_expression,"},{"line_number":525,"context_line":"                  datapoint, usage_data_metric_name)"},{"line_number":526,"context_line":""},{"line_number":527,"context_line":"        if not eval(skip_datapoint_expression):"},{"line_number":528,"context_line":"            filtered_datapoints.append(datapoint)"},{"line_number":529,"context_line":"        else:"},{"line_number":530,"context_line":"            LOG.debug(\"Datapoint [%s] will be skipped because of \""}],"source_content_type":"text/x-python","patch_set":5,"id":"3a28a49d_15cf7e0c","line":527,"in_reply_to":"303ebe06_b1465372","updated":"2025-12-12 16:53:28.000000000","message":"I disagree. It is not a user of CloudKitty; it is an operator. By operator, I mean a root cloud admin who should know how to configure and use the systems that are she/he is deploying. I know that there are a lot of limited operators trying/using this kind of software, but the target audience of this feature is not a normal/common user. \n\nIt is far more complicated other configurations (at least for), such as other systems, such as InfluxDB, Gnocchi, ElasticSearch, Loki, and so on. The point is that if users are unsure, they do not need to use this option.\n\nMoreover, if we start following the path of limiting what to do, we would need to have patches for every single option and operation that one needs to use. Also, on top of that, we have this precedence in other systems. Ceilometer is the basis for everything that has to do with CloudKitty, as data gathering starts there, and to use the Dynamic pollster sub-system (which is very interesting for one that wants to implement a billing pipeline), people need to work in this fashion to be able to properly create metrics that are then consumed/used in CloudKitty.","commit_id":"4746a09d8552d51170e1a1dc51db9f14bc33fef0"},{"author":{"_account_id":32968,"name":"Juan Larriba","email":"jlarriba@redhat.com","username":"jlarriba"},"change_message_id":"35963260ba3e8a33cd7bd418ff7e0a8f5991f8d4","unresolved":false,"context_lines":[{"line_number":524,"context_line":"                  \"[%s] under metric [%s].\", skip_datapoint_expression,"},{"line_number":525,"context_line":"                  datapoint, usage_data_metric_name)"},{"line_number":526,"context_line":""},{"line_number":527,"context_line":"        if not eval(skip_datapoint_expression):"},{"line_number":528,"context_line":"            filtered_datapoints.append(datapoint)"},{"line_number":529,"context_line":"        else:"},{"line_number":530,"context_line":"            LOG.debug(\"Datapoint [%s] will be skipped because of \""}],"source_content_type":"text/x-python","patch_set":5,"id":"d6a77535_c233c948","line":527,"in_reply_to":"3a28a49d_15cf7e0c","updated":"2025-12-16 07:25:45.000000000","message":"Well, it is another way to see it. With this kind of user/operator in mind, I don\u0027t see an issue with the patch.","commit_id":"4746a09d8552d51170e1a1dc51db9f14bc33fef0"}],"cloudkitty/tests/test_orchestrator.py":[{"author":{"_account_id":32968,"name":"Juan Larriba","email":"jlarriba@redhat.com","username":"jlarriba"},"change_message_id":"d96b979344ad75da3ade120726625e9a6ae98897","unresolved":true,"context_lines":[{"line_number":344,"context_line":"        ])"},{"line_number":345,"context_line":""},{"line_number":346,"context_line":"    @mock.patch(\u0027cloudkitty.dataframe.DataFrame.from_dict\u0027)"},{"line_number":347,"context_line":"    def test_persist_rating_data(self, dataframe_from_dict_mock):"},{"line_number":348,"context_line":"        start_time \u003d tzutils.localized_now()"},{"line_number":349,"context_line":"        end_time \u003d start_time + datetime.timedelta(hours\u003d1)"},{"line_number":350,"context_line":"        frame \u003d dataframe_from_dict_mock.return_value"}],"source_content_type":"text/x-python","patch_set":5,"id":"55419c14_378131ca","line":347,"updated":"2025-12-12 11:05:58.000000000","message":"This test does not test filtered logic, I would say that the actual feature being added by this patch is not being tested.","commit_id":"4746a09d8552d51170e1a1dc51db9f14bc33fef0"},{"author":{"_account_id":28356,"name":"Rafael Weingartner","email":"rafael@apache.org","username":"rafaelweingartner"},"change_message_id":"65faf9a55b5e36409003c2ccaee5c7dd0d3f5553","unresolved":false,"context_lines":[{"line_number":344,"context_line":"        ])"},{"line_number":345,"context_line":""},{"line_number":346,"context_line":"    @mock.patch(\u0027cloudkitty.dataframe.DataFrame.from_dict\u0027)"},{"line_number":347,"context_line":"    def test_persist_rating_data(self, dataframe_from_dict_mock):"},{"line_number":348,"context_line":"        start_time \u003d tzutils.localized_now()"},{"line_number":349,"context_line":"        end_time \u003d start_time + datetime.timedelta(hours\u003d1)"},{"line_number":350,"context_line":"        frame \u003d dataframe_from_dict_mock.return_value"}],"source_content_type":"text/x-python","patch_set":5,"id":"857e9ea0_69a1fe62","line":347,"in_reply_to":"55419c14_378131ca","updated":"2025-12-12 16:53:28.000000000","message":"yes, there are no tests for the feature being introduced. I will add it.","commit_id":"4746a09d8552d51170e1a1dc51db9f14bc33fef0"},{"author":{"_account_id":32968,"name":"Juan Larriba","email":"jlarriba@redhat.com","username":"jlarriba"},"change_message_id":"35963260ba3e8a33cd7bd418ff7e0a8f5991f8d4","unresolved":false,"context_lines":[{"line_number":344,"context_line":"        ])"},{"line_number":345,"context_line":""},{"line_number":346,"context_line":"    @mock.patch(\u0027cloudkitty.dataframe.DataFrame.from_dict\u0027)"},{"line_number":347,"context_line":"    def test_persist_rating_data(self, dataframe_from_dict_mock):"},{"line_number":348,"context_line":"        start_time \u003d tzutils.localized_now()"},{"line_number":349,"context_line":"        end_time \u003d start_time + datetime.timedelta(hours\u003d1)"},{"line_number":350,"context_line":"        frame \u003d dataframe_from_dict_mock.return_value"}],"source_content_type":"text/x-python","patch_set":5,"id":"f9711c1a_b026d8a7","line":347,"in_reply_to":"857e9ea0_69a1fe62","updated":"2025-12-16 07:25:45.000000000","message":"thank you!","commit_id":"4746a09d8552d51170e1a1dc51db9f14bc33fef0"}]}
