From fb552d129e29f2cb0abd51005416da015ba23441 Mon Sep 17 00:00:00 2001 From: Sh1nku <42642351+Sh1nku@users.noreply.github.com> Date: Sun, 30 Jun 2024 23:31:08 +0200 Subject: [PATCH 1/9] Move all request handling to common interface --- Cargo.toml | 1 + framework/src/queries/request_builder.rs | 4 +-- framework/src/queries/select.rs | 30 +++++++-------------- wrappers/python/Cargo.toml | 1 + wrappers/python/src/lib.rs | 2 ++ wrappers/python/tests/helpers.py | 11 +++++--- wrappers/python/tests/test_logging.py | 33 ++++++++++++++++++++++++ 7 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 wrappers/python/tests/test_logging.py diff --git a/Cargo.toml b/Cargo.toml index 15a2b3e..720c08c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,5 @@ regex = "1" dotenv = "0.15" pyo3 = "0.21" pyo3-asyncio = { package = "pyo3-asyncio-0-21", version = "0.21" } +pyo3-log = "0.10.0" pythonize = "0.21" \ No newline at end of file diff --git a/framework/src/queries/request_builder.rs b/framework/src/queries/request_builder.rs index 0036573..e798d21 100644 --- a/framework/src/queries/request_builder.rs +++ b/framework/src/queries/request_builder.rs @@ -61,9 +61,9 @@ impl<'a> SolrRequestBuilder<'a> { Ok(solr_response) } - pub async fn send_post_with_json( + pub async fn send_post_with_json( self, - json: T, + json: &T, ) -> Result { let mut request = create_standard_request( self.context, diff --git a/framework/src/queries/select.rs b/framework/src/queries/select.rs index b6a4685..176c519 100644 --- a/framework/src/queries/select.rs +++ b/framework/src/queries/select.rs @@ -1,11 +1,15 @@ +use serde::{Deserialize, Serialize}; + use crate::models::context::SolrServerContext; -use crate::models::error::{try_solr_error, SolrError}; +use crate::models::error::SolrError; use crate::models::response::SolrResponse; use crate::queries::components::facet_set::FacetSetComponent; use crate::queries::components::grouping::GroupingComponent; use crate::queries::components::json_facet::JsonFacetComponent; use crate::queries::def_type::DefType; -use serde::{Deserialize, Serialize}; +use crate::queries::request_builder::SolrRequestBuilder; +#[cfg(feature = "blocking")] +use crate::runtime::RUNTIME; #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)] struct PostQueryWrapper { @@ -256,31 +260,17 @@ impl SelectQuery { context: C, collection: T, ) -> Result { - let solr_url = format!( - "{}/solr/{}/{}", - context.as_ref().host.get_solr_node().await?, - collection.as_ref(), - &self.handle - ); + let solr_url = format!("/solr/{}/{}", collection.as_ref(), &self.handle); let wrapper = PostQueryWrapper { params: self.clone(), }; - let mut request = context - .as_ref() - .client - .post(&solr_url) - .json::(&wrapper); - if let Some(auth) = &context.as_ref().auth { - request = auth.add_auth_to_request(request); - } - let data = request.send().await?.json::().await?; - try_solr_error(&data)?; + let data = SolrRequestBuilder::new(context.as_ref(), solr_url.as_str()) + .send_post_with_json::(&wrapper) + .await?; Ok(data) } } -#[cfg(feature = "blocking")] -use crate::runtime::RUNTIME; #[cfg(feature = "blocking")] impl SelectQuery { pub fn execute_blocking, S: AsRef>( diff --git a/wrappers/python/Cargo.toml b/wrappers/python/Cargo.toml index 8229493..a740ca5 100644 --- a/wrappers/python/Cargo.toml +++ b/wrappers/python/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { workspace = true, features = ["extension-module", "serde", "abi3-py38"] } pyo3-asyncio = { workspace = true, features = ["attributes", "tokio-runtime"] } +pyo3-log = "0.10.0" pythonize.workspace = true solrstice = { workspace = true, features = ["blocking"] } serde.workspace = true diff --git a/wrappers/python/src/lib.rs b/wrappers/python/src/lib.rs index 2537de0..fc5a0fc 100644 --- a/wrappers/python/src/lib.rs +++ b/wrappers/python/src/lib.rs @@ -33,6 +33,8 @@ fn queries_module(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { /// A Python module implemented in Rust. #[pymodule] fn solrstice(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + pyo3_log::init(); + let sys = PyModule::import_bound(_py, "sys")?; let sys_modules = sys.getattr("modules")?; let sys_modules: &Bound<'_, PyDict> = sys_modules.downcast()?; diff --git a/wrappers/python/tests/helpers.py b/wrappers/python/tests/helpers.py index da81198..b8806da 100644 --- a/wrappers/python/tests/helpers.py +++ b/wrappers/python/tests/helpers.py @@ -27,12 +27,15 @@ class Config: async_client: AsyncSolrCloudClient -def create_config() -> Config: +def get_path_prefix() -> str: path_prefix = "../../" if not os.path.exists(os.path.join(path_prefix, "test_setup/.env")): path_prefix = "../../../" + return path_prefix + - path = os.path.join(path_prefix, "test_setup/.env") +def create_config() -> Config: + path = os.path.join(get_path_prefix(), "test_setup/.env") load_dotenv(path) solr_auth = None solr_username = os.getenv("SOLR_USERNAME") @@ -50,7 +53,7 @@ def create_config() -> Config: solr_username, solr_password, context, - os.path.join(path_prefix, "test_setup/test_collection"), + os.path.join(get_path_prefix(), "test_setup/test_collection"), AsyncSolrCloudClient(context), ) @@ -91,7 +94,7 @@ class City(DataClassJsonMixin): def load_test_data() -> List[City]: - with open("../../test_setup/test_data.json") as f: + with open(os.path.join(get_path_prefix(), "test_setup/test_data.json")) as f: return City.schema().loads(f.read(), many=True) diff --git a/wrappers/python/tests/test_logging.py b/wrappers/python/tests/test_logging.py new file mode 100644 index 0000000..108fac2 --- /dev/null +++ b/wrappers/python/tests/test_logging.py @@ -0,0 +1,33 @@ +import logging + +import pytest +from _pytest.logging import LogCaptureFixture + +from helpers import Config, create_config, wait_for_solr, setup_collection, index_test_data, teardown_collection +from solrstice.queries import SelectQuery + + +@pytest.fixture() +def config() -> Config: + yield create_config() + + +@pytest.mark.asyncio +async def test_sending_select_query_writes_message(config: Config, caplog: LogCaptureFixture): + caplog.set_level(logging.DEBUG) + name = "SendingSelectQueryWritesMessage" + wait_for_solr(config.solr_host, 30) + + try: + await setup_collection(config.context, name, config.config_path) + + await index_test_data(config.context, name) + + builder = SelectQuery() + await builder.execute(config.context, name) + + for record in caplog.records: + print(record) + + finally: + await teardown_collection(config.context, name) From 66db8b7207a8dadc68cd3646b1f6854a6180ce77 Mon Sep 17 00:00:00 2001 From: Sh1nku <42642351+Sh1nku@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:26:51 +0200 Subject: [PATCH 2/9] Fix cargo clippy errors --- framework/src/clients/blocking_cloud_client.rs | 2 +- framework/src/models/context.rs | 2 +- framework/src/models/response.rs | 4 ++-- framework/src/queries/collection.rs | 2 +- framework/src/queries/config.rs | 6 ++---- framework/src/queries/request_builder.rs | 14 +++++++------- wrappers/python/src/models/facet_set.rs | 2 +- wrappers/python/src/models/group.rs | 6 +++--- wrappers/python/src/models/json_facet.rs | 2 +- wrappers/python/src/models/response.rs | 6 ++---- wrappers/python/src/queries/alias.rs | 12 +++++------- wrappers/python/src/queries/collection.rs | 4 ++-- .../python/src/queries/components/facet_set.rs | 1 + wrappers/python/src/queries/components/grouping.rs | 1 + .../python/src/queries/components/json_facet.rs | 1 + wrappers/python/src/queries/config.rs | 8 ++++---- wrappers/python/src/queries/def_type.rs | 2 ++ wrappers/python/src/queries/select.rs | 1 + 18 files changed, 38 insertions(+), 38 deletions(-) diff --git a/framework/src/clients/blocking_cloud_client.rs b/framework/src/clients/blocking_cloud_client.rs index df30f92..14c53ff 100644 --- a/framework/src/clients/blocking_cloud_client.rs +++ b/framework/src/clients/blocking_cloud_client.rs @@ -180,7 +180,7 @@ impl BlockingSolrCloudClient { /// # Ok(()) /// # } /// ``` - pub fn collection_exists<'a, S: AsRef>(&self, name: S) -> Result { + pub fn collection_exists>(&self, name: S) -> Result { collection_exists_blocking(&self.context, name) } diff --git a/framework/src/models/context.rs b/framework/src/models/context.rs index c9d5d78..7dcb1a6 100644 --- a/framework/src/models/context.rs +++ b/framework/src/models/context.rs @@ -106,7 +106,7 @@ impl From for SolrServerContext { Self { host: builder.host, auth: builder.auth, - client: builder.client.unwrap_or_else(reqwest::Client::new), + client: builder.client.unwrap_or_default(), } } } diff --git a/framework/src/models/response.rs b/framework/src/models/response.rs index 6b86a24..c182918 100644 --- a/framework/src/models/response.rs +++ b/framework/src/models/response.rs @@ -188,8 +188,8 @@ where Some(value_map) => { let mut return_map: HashMap> = HashMap::new(); for (key, values) in value_map { - if values.len() > 0 { - return_map.insert(key, values.split(",").map(|x| x.to_string()).collect()); + if !values.is_empty() { + return_map.insert(key, values.split(',').map(|x| x.to_string()).collect()); } else { return_map.insert(key, vec![]); } diff --git a/framework/src/queries/collection.rs b/framework/src/queries/collection.rs index c5ad885..7f8cd09 100644 --- a/framework/src/queries/collection.rs +++ b/framework/src/queries/collection.rs @@ -84,7 +84,7 @@ pub fn get_collections_blocking>( } #[cfg(feature = "blocking")] -pub fn collection_exists_blocking<'a, C: AsRef, S: AsRef>( +pub fn collection_exists_blocking, S: AsRef>( context: C, name: S, ) -> Result { diff --git a/framework/src/queries/config.rs b/framework/src/queries/config.rs index f807f1d..56c4501 100644 --- a/framework/src/queries/config.rs +++ b/framework/src/queries/config.rs @@ -1,5 +1,5 @@ use crate::models::context::SolrServerContext; -use crate::models::error::{try_solr_error, SolrError}; +use crate::models::error::SolrError; use crate::queries::request_builder::SolrRequestBuilder; use std::fs::File; use std::io::{Read, Seek, Write}; @@ -63,13 +63,11 @@ pub async fn upload_config, S: AsRef, P: AsRef< let mut vec = Vec::new(); outfile.read_to_end(&mut vec)?; - let json = SolrRequestBuilder::new(context.as_ref(), "/solr/admin/configs") + let _ = SolrRequestBuilder::new(context.as_ref(), "/solr/admin/configs") .with_query_params(query_params.as_ref()) .with_headers(vec![("Content-Type", "application/octet-stream")]) .send_post_with_body(vec) .await?; - - try_solr_error(&json)?; Ok(()) } diff --git a/framework/src/queries/request_builder.rs b/framework/src/queries/request_builder.rs index e798d21..ecddb59 100644 --- a/framework/src/queries/request_builder.rs +++ b/framework/src/queries/request_builder.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug, Copy, Clone)] enum SolrRequestType { - GET, - POST, + Get, + Post, } pub struct SolrRequestBuilder<'a> { @@ -49,7 +49,7 @@ impl<'a> SolrRequestBuilder<'a> { let request = create_standard_request( self.context, self.url, - SolrRequestType::GET, + SolrRequestType::Get, self.query_params, self.headers.as_ref(), ) @@ -68,7 +68,7 @@ impl<'a> SolrRequestBuilder<'a> { let mut request = create_standard_request( self.context, self.url, - SolrRequestType::POST, + SolrRequestType::Post, self.query_params, self.headers.as_ref(), ) @@ -88,7 +88,7 @@ impl<'a> SolrRequestBuilder<'a> { let mut request = create_standard_request( self.context, self.url, - SolrRequestType::POST, + SolrRequestType::Post, self.query_params, self.headers.as_ref(), ) @@ -110,12 +110,12 @@ async fn create_standard_request<'a>( headers: Option<&Vec<(String, String)>>, ) -> Result { let mut request = match request_type { - SolrRequestType::GET => { + SolrRequestType::Get => { context .client .get(format!("{}{}", context.host.get_solr_node().await?, url)) } - SolrRequestType::POST => { + SolrRequestType::Post => { context .client .post(format!("{}{}", context.host.get_solr_node().await?, url)) diff --git a/wrappers/python/src/models/facet_set.rs b/wrappers/python/src/models/facet_set.rs index 6a87cdc..78ea45f 100644 --- a/wrappers/python/src/models/facet_set.rs +++ b/wrappers/python/src/models/facet_set.rs @@ -95,7 +95,7 @@ impl SolrPivotFacetResultWrapper { } pub fn get_pivots(&self) -> Vec { - self.0.get_pivots().into_iter().map(|x| x.into()).collect() + self.0.get_pivots().iter().map(|x| x.into()).collect() } pub fn get_queries(&self) -> HashMap { diff --git a/wrappers/python/src/models/group.rs b/wrappers/python/src/models/group.rs index b10c2ef..f164ae8 100644 --- a/wrappers/python/src/models/group.rs +++ b/wrappers/python/src/models/group.rs @@ -31,19 +31,19 @@ impl SolrGroupResultWrapper { pub fn get_field_result(&self) -> Option> { self.0 .get_field_result() - .map(|v| v.into_iter().map(|v| v.to_owned().into()).collect()) + .map(|v| v.iter().map(|v| v.to_owned().into()).collect()) } pub fn get_query_result(&self) -> PyResult> { match self.0.get_query_result() { - Some(v) => Ok(Some(SolrDocsResponseWrapper::try_from(v.to_owned())?)), + Some(v) => Ok(Some(SolrDocsResponseWrapper::from(v.to_owned()))), None => Ok(None), } } pub fn get_simple_result(&self) -> PyResult> { match self.0.get_simple_result() { - Some(v) => Ok(Some(SolrDocsResponseWrapper::try_from(v.to_owned())?)), + Some(v) => Ok(Some(SolrDocsResponseWrapper::from(v.to_owned()))), None => Ok(None), } } diff --git a/wrappers/python/src/models/json_facet.rs b/wrappers/python/src/models/json_facet.rs index 46377de..0729dbe 100644 --- a/wrappers/python/src/models/json_facet.rs +++ b/wrappers/python/src/models/json_facet.rs @@ -32,7 +32,7 @@ impl SolrJsonFacetResponseWrapper { pub fn get_buckets(&self) -> Vec { self.0 .get_buckets() - .map(|bucket| SolrJsonFacetResponseWrapper::from(bucket)) + .map(SolrJsonFacetResponseWrapper::from) .collect() } diff --git a/wrappers/python/src/models/response.rs b/wrappers/python/src/models/response.rs index be3480f..52e6525 100644 --- a/wrappers/python/src/models/response.rs +++ b/wrappers/python/src/models/response.rs @@ -64,11 +64,9 @@ impl SolrDocsResponseWrapper { .0 .get_docs::() .map_err(PyErrWrapper::from)?; - let vec = docs - .into_iter() + docs.into_iter() .map(|doc| pythonize(py, &doc).map_err(PyErrWrapper::from)) - .collect::, _>>(); - vec + .collect::, _>>() }) .map_err(|e| e.into()) } diff --git a/wrappers/python/src/queries/alias.rs b/wrappers/python/src/queries/alias.rs index 744cc70..2e17421 100644 --- a/wrappers/python/src/queries/alias.rs +++ b/wrappers/python/src/queries/alias.rs @@ -58,7 +58,7 @@ pub fn create_alias( ) -> PyResult> { pyo3_asyncio::tokio::future_into_py(py, async move { let context: SolrServerContext = context.into(); - let result = create_alias_rs( + Ok(create_alias_rs( &context, name.as_str(), collections @@ -68,8 +68,7 @@ pub fn create_alias( .as_slice(), ) .await - .map_err(PyErrWrapper::from)?; - Ok(result) + .map_err(PyErrWrapper::from)?) }) } @@ -82,7 +81,7 @@ pub fn create_alias_blocking( ) -> PyResult<()> { py.allow_threads(move || { let context: SolrServerContext = context.into(); - let result = create_alias_blocking_rs( + Ok(create_alias_blocking_rs( &context, name.as_str(), collections @@ -91,8 +90,7 @@ pub fn create_alias_blocking( .collect::>() .as_slice(), ) - .map_err(PyErrWrapper::from)?; - Ok(result) + .map_err(PyErrWrapper::from)?) }) } @@ -136,7 +134,7 @@ pub fn delete_alias( delete_alias_rs(&context, name.as_str()) .await .map_err(PyErrWrapper::from)?; - Ok(Python::with_gil(|_| ())) + Ok(()) }) } diff --git a/wrappers/python/src/queries/collection.rs b/wrappers/python/src/queries/collection.rs index 7270b2f..9669b1a 100644 --- a/wrappers/python/src/queries/collection.rs +++ b/wrappers/python/src/queries/collection.rs @@ -38,7 +38,7 @@ pub fn create_collection( ) -> PyResult> { pyo3_asyncio::tokio::future_into_py(py, async move { let context: SolrServerContext = context.into(); - let result = create_collection_rs( + create_collection_rs( &context, name.as_str(), config.as_str(), @@ -47,7 +47,7 @@ pub fn create_collection( ) .await .map_err(PyErrWrapper::from)?; - Ok(Python::with_gil(|_| result)) + Ok(()) }) } diff --git a/wrappers/python/src/queries/components/facet_set.rs b/wrappers/python/src/queries/components/facet_set.rs index f07941e..cf27e89 100644 --- a/wrappers/python/src/queries/components/facet_set.rs +++ b/wrappers/python/src/queries/components/facet_set.rs @@ -194,6 +194,7 @@ pub struct FieldFacetEntryWrapper(FieldFacetEntry); #[pymethods] impl FieldFacetEntryWrapper { #[new] + #[allow(clippy::too_many_arguments)] pub fn new( field: String, prefix: Option, diff --git a/wrappers/python/src/queries/components/grouping.rs b/wrappers/python/src/queries/components/grouping.rs index bb1c369..f7d4b0e 100644 --- a/wrappers/python/src/queries/components/grouping.rs +++ b/wrappers/python/src/queries/components/grouping.rs @@ -46,6 +46,7 @@ impl From for GroupFormattingWrapper { #[pymethods] impl GroupingComponentWrapper { #[new] + #[allow(clippy::too_many_arguments)] pub fn new( fields: Option>, queries: Option>, diff --git a/wrappers/python/src/queries/components/json_facet.rs b/wrappers/python/src/queries/components/json_facet.rs index 44bb9f9..c5360e1 100644 --- a/wrappers/python/src/queries/components/json_facet.rs +++ b/wrappers/python/src/queries/components/json_facet.rs @@ -113,6 +113,7 @@ pub struct JsonQueryFacetWrapper {} #[pymethods] impl JsonQueryFacetWrapper { #[new] + #[allow(clippy::too_many_arguments)] fn new( q: String, limit: Option, diff --git a/wrappers/python/src/queries/config.rs b/wrappers/python/src/queries/config.rs index 7e99c33..a4f4634 100644 --- a/wrappers/python/src/queries/config.rs +++ b/wrappers/python/src/queries/config.rs @@ -37,10 +37,10 @@ pub fn upload_config( ) -> PyResult> { pyo3_asyncio::tokio::future_into_py(py, async move { let context: SolrServerContext = context.into(); - let result = upload_config_rs(&context, name.as_str(), path.as_path()) + upload_config_rs(&context, name.as_str(), path.as_path()) .await .map_err(PyErrWrapper::from)?; - Ok(Python::with_gil(|_| result)) + Ok(()) }) } @@ -117,10 +117,10 @@ pub fn delete_config( ) -> PyResult> { pyo3_asyncio::tokio::future_into_py(py, async move { let context: SolrServerContext = context.into(); - let result = delete_config_rs(&context, name.as_str()) + delete_config_rs(&context, name.as_str()) .await .map_err(PyErrWrapper::from)?; - Ok(Python::with_gil(|_| result)) + Ok(()) }) } diff --git a/wrappers/python/src/queries/def_type.rs b/wrappers/python/src/queries/def_type.rs index caa749c..2632bfb 100644 --- a/wrappers/python/src/queries/def_type.rs +++ b/wrappers/python/src/queries/def_type.rs @@ -92,6 +92,7 @@ pub struct DismaxQueryWrapper {} #[pymethods] impl DismaxQueryWrapper { #[new] + #[allow(clippy::too_many_arguments)] pub fn new( q_alt: Option, qf: Option, @@ -124,6 +125,7 @@ pub struct EdismaxQueryWrapper {} #[pymethods] impl EdismaxQueryWrapper { #[new] + #[allow(clippy::too_many_arguments)] pub fn new( q_alt: Option, qf: Option, diff --git a/wrappers/python/src/queries/select.rs b/wrappers/python/src/queries/select.rs index 12c2a6e..ad97b43 100644 --- a/wrappers/python/src/queries/select.rs +++ b/wrappers/python/src/queries/select.rs @@ -23,6 +23,7 @@ pub struct SelectQueryWrapper(SelectQuery); #[pymethods] impl SelectQueryWrapper { #[new] + #[allow(clippy::too_many_arguments)] fn new( q: Option, fl: Option>, From 34d0b45f28a3eecf5c0ac43e79b12a5bd60e8bba Mon Sep 17 00:00:00 2001 From: Sh1nku <42642351+Sh1nku@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:43:30 +0200 Subject: [PATCH 3/9] Add logging to requests --- Cargo.toml | 3 +- framework/Cargo.toml | 1 + framework/src/models/context.rs | 23 +++++ framework/src/queries/request_builder.rs | 96 ++++++++++++++++--- framework/tests/functionality/logging_test.rs | 86 +++++++++++++++++ framework/tests/functionality/mod.rs | 1 + 6 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 framework/tests/functionality/logging_test.rs diff --git a/Cargo.toml b/Cargo.toml index 720c08c..cdf6160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,5 @@ dotenv = "0.15" pyo3 = "0.21" pyo3-asyncio = { package = "pyo3-asyncio-0-21", version = "0.21" } pyo3-log = "0.10.0" -pythonize = "0.21" \ No newline at end of file +pythonize = "0.21" +env_logger = "0.11.3" \ No newline at end of file diff --git a/framework/Cargo.toml b/framework/Cargo.toml index 4f2918b..ba6d870 100644 --- a/framework/Cargo.toml +++ b/framework/Cargo.toml @@ -33,3 +33,4 @@ blocking = ["tokio"] [dev-dependencies] tokio = { features = ["macros", "rt", "rt-multi-thread"], workspace = true } dotenv.workspace = true +env_logger.workspace = true \ No newline at end of file diff --git a/framework/src/models/context.rs b/framework/src/models/context.rs index 7dcb1a6..f3b7ba3 100644 --- a/framework/src/models/context.rs +++ b/framework/src/models/context.rs @@ -1,5 +1,6 @@ use crate::hosts::solr_host::SolrHost; use crate::models::auth::SolrAuth; +use crate::queries::request_builder::LoggingPolicy; use std::sync::Arc; /// A SolrServerContext specifies how to connect to a solr server, and how to authenticate. @@ -19,6 +20,7 @@ pub struct SolrServerContextBuilder { pub(crate) host: Arc, pub(crate) auth: Option>, pub(crate) client: Option, + pub(crate) logging_policy: LoggingPolicy, } impl SolrServerContextBuilder { @@ -34,6 +36,7 @@ impl SolrServerContextBuilder { host: Arc::new(host), auth: None, client: None, + logging_policy: LoggingPolicy::Fast(512), } } @@ -67,6 +70,24 @@ impl SolrServerContextBuilder { self } + /// Set a logging policy + /// The accepted values are `Logging::Off`, `Logging::Fast(usize)`, `Logging::Pretty(usize)` + /// `usize` is the maximum length of the body that will be logged in bytes + /// `Pretty` is an expensive + /// # Examples + /// ``` + /// use solrstice::models::context::SolrServerContextBuilder; + /// use solrstice::hosts::solr_server_host::SolrSingleServerHost; + /// use solrstice::models::auth::SolrBasicAuth; + /// use solrstice::queries::request_builder::LoggingPolicy; + /// let context = SolrServerContextBuilder::new(SolrSingleServerHost::new("http://localhost:8983")) + /// .with_logging(LoggingPolicy::Fast(4096)) + /// .build(); + pub fn with_logging_policy(mut self, logging_policy: LoggingPolicy) -> Self { + self.logging_policy = logging_policy; + self + } + /// Build a SolrServerContext /// # Examples /// ```no_run @@ -99,6 +120,7 @@ pub struct SolrServerContext { pub(crate) host: Arc, pub(crate) auth: Option>, pub(crate) client: reqwest::Client, + pub(crate) logging_policy: LoggingPolicy, } impl From for SolrServerContext { @@ -107,6 +129,7 @@ impl From for SolrServerContext { host: builder.host, auth: builder.auth, client: builder.client.unwrap_or_default(), + logging_policy: builder.logging_policy, } } } diff --git a/framework/src/queries/request_builder.rs b/framework/src/queries/request_builder.rs index ecddb59..ca84693 100644 --- a/framework/src/queries/request_builder.rs +++ b/framework/src/queries/request_builder.rs @@ -1,7 +1,9 @@ use crate::models::context::SolrServerContext; use crate::models::error::{try_solr_error, SolrError}; use crate::models::response::SolrResponse; -use reqwest::{Body, RequestBuilder, Response}; +use log::debug; +use reqwest::{Body, Request, RequestBuilder, Response}; +use serde::__private::from_utf8_lossy; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug, Copy, Clone)] @@ -10,6 +12,16 @@ enum SolrRequestType { Post, } +/// How detailed the logs of the requests should be +/// For `Fast` and `Pretty` the number is the maximum length of the body that will be logged +/// Logging will be with the `debug` level +#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub enum LoggingPolicy { + Off, + Fast(usize), + Pretty(usize), +} + pub struct SolrRequestBuilder<'a> { context: &'a SolrServerContext, url: &'a str, @@ -54,7 +66,12 @@ impl<'a> SolrRequestBuilder<'a> { self.headers.as_ref(), ) .await?; - let response = request.send().await?; + + let (client, request) = request.build_split(); + let request = request?; + log_request_info(&request, self.context.logging_policy); + + let response = client.execute(request).await?; try_request_auth_error(&response).await?; let solr_response = response.json::().await?; try_solr_error(&solr_response)?; @@ -74,7 +91,12 @@ impl<'a> SolrRequestBuilder<'a> { ) .await?; request = request.json(&json); - let response = request.send().await?; + + let (client, request) = request.build_split(); + let request = request?; + log_request_info(&request, self.context.logging_policy); + + let response = client.execute(request).await?; try_request_auth_error(&response).await?; let solr_response = response.json::().await?; try_solr_error(&solr_response)?; @@ -94,7 +116,12 @@ impl<'a> SolrRequestBuilder<'a> { ) .await?; request = request.body(data.into()); - let response = request.send().await?; + + let (client, request) = request.build_split(); + let request = request?; + log_request_info(&request, self.context.logging_policy); + + let response = client.execute(request).await?; try_request_auth_error(&response).await?; let solr_response = response.json::().await?; try_solr_error(&solr_response)?; @@ -109,17 +136,10 @@ async fn create_standard_request<'a>( query_params: Option<&'a [(&'a str, &'a str)]>, headers: Option<&Vec<(String, String)>>, ) -> Result { + let url = format!("{}{}", context.host.get_solr_node().await?, url); let mut request = match request_type { - SolrRequestType::Get => { - context - .client - .get(format!("{}{}", context.host.get_solr_node().await?, url)) - } - SolrRequestType::Post => { - context - .client - .post(format!("{}{}", context.host.get_solr_node().await?, url)) - } + SolrRequestType::Get => context.client.get(url), + SolrRequestType::Post => context.client.post(url), }; if let Some(query_params) = query_params { request = request.query(query_params); @@ -153,3 +173,51 @@ async fn try_request_auth_error(response: &Response) -> Result<(), SolrError> { } } } + +static NO_BODY: &[u8] = "No body".as_bytes(); +static ERROR_BODY: &str = "Error while getting body"; +static TOO_BIG_BODY: &str = "Body too big to log"; + +fn log_request_info(request: &Request, logging: LoggingPolicy) { + if logging == LoggingPolicy::Off { + return; + } + let url = request.url(); + let headers = request.headers(); + let body = request.body().map(|b| b.as_bytes().unwrap_or_default()); + let body = body.unwrap_or(NO_BODY); + match logging { + LoggingPolicy::Fast(max) => { + let body = if body.len() > max { + TOO_BIG_BODY.as_bytes() + } else { + body + }; + debug!( + "Sending Solr request to {}\nHeaders: {:?}\nBody: {}", + url, + headers, + from_utf8_lossy(body) + ); + } + LoggingPolicy::Pretty(max) => { + let body = match body.len() > max { + true => TOO_BIG_BODY.to_string(), + false => { + let body = serde_json::from_slice::(body); + match body { + Ok(body) => { + serde_json::to_string_pretty(&body).unwrap_or(ERROR_BODY.to_string()) + } + Err(_) => ERROR_BODY.to_string(), + } + } + }; + debug!( + "Sending Solr request to {}\nHeaders: {:?}\nBody:\n{}", + url, headers, body + ); + } + _ => {} + } +} diff --git a/framework/tests/functionality/logging_test.rs b/framework/tests/functionality/logging_test.rs new file mode 100644 index 0000000..2f4ee16 --- /dev/null +++ b/framework/tests/functionality/logging_test.rs @@ -0,0 +1,86 @@ +use crate::structures::BaseTestsBuildup; +use log::{Metadata, Record}; +use solrstice::clients::async_cloud_client::AsyncSolrCloudClient; +use solrstice::models::context::SolrServerContextBuilder; +use solrstice::models::error::SolrError; +use solrstice::queries::request_builder::LoggingPolicy; +use std::sync::{Arc, Mutex}; + +struct TestLogger { + messages: Arc>>, +} + +impl log::Log for TestLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::Level::Debug + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let mut messages = self.messages.lock().unwrap(); + messages.push(format!("{} - {}", record.level(), record.args())); + } + } + + fn flush(&self) {} +} + +#[tokio::test] +async fn logging_logs_message() -> Result<(), SolrError> { + let messages = Arc::new(Mutex::new(Vec::new())); + let logger = TestLogger { + messages: Arc::clone(&messages), + }; + log::set_boxed_logger(Box::new(logger)).unwrap(); + log::set_max_level(log::LevelFilter::Debug); + + let config = BaseTestsBuildup::new().await; + let mut context = SolrServerContextBuilder::new(config.host); + if config.auth.is_some() { + context = context.with_auth(config.auth.unwrap()); + } + let context = context.build(); + let client = AsyncSolrCloudClient::new(context); + + messages.lock().unwrap().clear(); + + let _ = client.get_configs().await.unwrap(); + for message in &*messages.lock().unwrap() { + if message.contains("Sending Solr request to") { + return Ok(()); + } + } + Err(SolrError::Unknown("No log message found".to_string())) +} + +#[tokio::test] +async fn logging_does_not_log_message_if_disabled() -> Result<(), SolrError> { + let messages = Arc::new(Mutex::new(Vec::new())); + let logger = TestLogger { + messages: Arc::clone(&messages), + }; + log::set_boxed_logger(Box::new(logger)).unwrap(); + log::set_max_level(log::LevelFilter::Debug); + + let config = BaseTestsBuildup::new().await; + let mut context = + SolrServerContextBuilder::new(config.host).with_logging_policy(LoggingPolicy::Off); + if config.auth.is_some() { + context = context.with_auth(config.auth.unwrap()); + } + let context = context.build(); + let client = AsyncSolrCloudClient::new(context); + + messages.lock().unwrap().clear(); + + let _ = client.get_configs().await.unwrap(); + for message in &*messages.lock().unwrap() { + if message.contains("Sending Solr request to") { + return Err(SolrError::Unknown(format!( + "Did not expect log message, but got {}", + message + ))); + } + } + Ok(()) +} diff --git a/framework/tests/functionality/mod.rs b/framework/tests/functionality/mod.rs index c6ace76..1d7941b 100644 --- a/framework/tests/functionality/mod.rs +++ b/framework/tests/functionality/mod.rs @@ -15,3 +15,4 @@ pub mod auth_test; #[cfg(feature = "blocking")] pub mod blocking_tests; +mod logging_test; From c9276bd20c17568c34f23ba4485e98d1d29c69c4 Mon Sep 17 00:00:00 2001 From: Sh1nku <42642351+Sh1nku@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:54:33 +0200 Subject: [PATCH 4/9] Mark tests as parallel or serial to avoid interference --- Cargo.toml | 3 +- framework/Cargo.toml | 1 + framework/src/models/context.rs | 2 +- framework/tests/functionality/alias_tests.rs | 2 + framework/tests/functionality/auth_test.rs | 3 ++ .../tests/functionality/blocking_tests.rs | 2 + framework/tests/functionality/client_tests.rs | 3 ++ .../tests/functionality/collection_test.rs | 2 + framework/tests/functionality/config_test.rs | 6 +++ .../tests/functionality/def_type_test.rs | 4 ++ .../tests/functionality/facetset_test.rs | 6 +++ .../tests/functionality/grouping_tests.rs | 7 ++++ framework/tests/functionality/index_test.rs | 4 ++ .../tests/functionality/json_facet_test.rs | 5 +++ framework/tests/functionality/logging_test.rs | 42 ++++++++++++------- framework/tests/functionality/readme_test.rs | 2 + framework/tests/functionality/select_test.rs | 4 ++ framework/tests/functionality/zk_test.rs | 3 ++ 18 files changed, 84 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cdf6160..785dde3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,4 +33,5 @@ pyo3 = "0.21" pyo3-asyncio = { package = "pyo3-asyncio-0-21", version = "0.21" } pyo3-log = "0.10.0" pythonize = "0.21" -env_logger = "0.11.3" \ No newline at end of file +env_logger = "0.11.3" +serial_test = "3.1.1" \ No newline at end of file diff --git a/framework/Cargo.toml b/framework/Cargo.toml index ba6d870..5b294ca 100644 --- a/framework/Cargo.toml +++ b/framework/Cargo.toml @@ -32,5 +32,6 @@ blocking = ["tokio"] [dev-dependencies] tokio = { features = ["macros", "rt", "rt-multi-thread"], workspace = true } +serial_test.workspace = true dotenv.workspace = true env_logger.workspace = true \ No newline at end of file diff --git a/framework/src/models/context.rs b/framework/src/models/context.rs index f3b7ba3..deea64d 100644 --- a/framework/src/models/context.rs +++ b/framework/src/models/context.rs @@ -81,7 +81,7 @@ impl SolrServerContextBuilder { /// use solrstice::models::auth::SolrBasicAuth; /// use solrstice::queries::request_builder::LoggingPolicy; /// let context = SolrServerContextBuilder::new(SolrSingleServerHost::new("http://localhost:8983")) - /// .with_logging(LoggingPolicy::Fast(4096)) + /// .with_logging_policy(LoggingPolicy::Fast(4096)) /// .build(); pub fn with_logging_policy(mut self, logging_policy: LoggingPolicy) -> Self { self.logging_policy = logging_policy; diff --git a/framework/tests/functionality/alias_tests.rs b/framework/tests/functionality/alias_tests.rs index d115b71..1761306 100644 --- a/framework/tests/functionality/alias_tests.rs +++ b/framework/tests/functionality/alias_tests.rs @@ -1,10 +1,12 @@ use crate::structures::BaseTestsBuildup; +use serial_test::parallel; use solrstice::queries::alias::{alias_exists, create_alias, delete_alias}; use solrstice::queries::collection::{create_collection, delete_collection}; use solrstice::queries::config::{delete_config, upload_config}; use std::path::Path; #[tokio::test] +#[parallel] async fn create_alias_creates_alias() { let alias_name = "CreateAliasAlias".to_string(); let collection_name = "CreateAliasCollection".to_string(); diff --git a/framework/tests/functionality/auth_test.rs b/framework/tests/functionality/auth_test.rs index 8be3f79..dd333b3 100644 --- a/framework/tests/functionality/auth_test.rs +++ b/framework/tests/functionality/auth_test.rs @@ -1,10 +1,12 @@ use crate::structures::BaseTestsBuildup; +use serial_test::parallel; use solrstice::clients::async_cloud_client::AsyncSolrCloudClient; use solrstice::models::auth::SolrBasicAuth; use solrstice::models::context::SolrServerContextBuilder; use solrstice::models::error::SolrError; #[tokio::test] +#[parallel] async fn auth_gives_sensible_error_when_not_provided() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; if config.auth.is_none() { @@ -25,6 +27,7 @@ async fn auth_gives_sensible_error_when_not_provided() -> Result<(), SolrError> } #[tokio::test] +#[parallel] async fn auth_gives_sensible_error_when_wrong() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; if config.auth.is_none() { diff --git a/framework/tests/functionality/blocking_tests.rs b/framework/tests/functionality/blocking_tests.rs index fca9808..2aa8a17 100644 --- a/framework/tests/functionality/blocking_tests.rs +++ b/framework/tests/functionality/blocking_tests.rs @@ -1,8 +1,10 @@ use crate::structures::FunctionalityTestsBuildup; +use serial_test::parallel; use solrstice::queries::config::get_configs_blocking; use std::thread; #[test] +#[parallel] fn blocking_works_when_simultaneous_connections_multiple_threads() { let runtime = tokio::runtime::Runtime::new().unwrap(); let config = runtime.block_on(async { diff --git a/framework/tests/functionality/client_tests.rs b/framework/tests/functionality/client_tests.rs index 0866fe6..b00b4eb 100644 --- a/framework/tests/functionality/client_tests.rs +++ b/framework/tests/functionality/client_tests.rs @@ -1,5 +1,6 @@ use crate::structures::BaseTestsBuildup; use serde::{Deserialize, Serialize}; +use serial_test::parallel; use solrstice::clients::async_cloud_client::AsyncSolrCloudClient; use solrstice::queries::index::{DeleteQuery, UpdateQuery}; use solrstice::queries::select::SelectQuery; @@ -12,6 +13,7 @@ struct TestData { } #[tokio::test] +#[parallel] pub async fn client_example_test() { let name = "example_test"; let config = BaseTestsBuildup::new().await; @@ -61,6 +63,7 @@ pub async fn client_example_test() { } #[tokio::test] +#[parallel] pub async fn multiple_clients_test() { let name = "multiple_clients_test".to_string(); let config_1 = BaseTestsBuildup::new().await; diff --git a/framework/tests/functionality/collection_test.rs b/framework/tests/functionality/collection_test.rs index 181ccde..3aeda4f 100644 --- a/framework/tests/functionality/collection_test.rs +++ b/framework/tests/functionality/collection_test.rs @@ -1,9 +1,11 @@ use crate::structures::BaseTestsBuildup; +use serial_test::parallel; use solrstice::queries::collection::{collection_exists, create_collection, delete_collection}; use solrstice::queries::config::{delete_config, upload_config}; use std::path::Path; #[tokio::test] +#[parallel] async fn create_collection_creates_collection() { let config_name = "CreateCollectionConfig".to_string(); let collection_name = "CreateCollectionCollection".to_string(); diff --git a/framework/tests/functionality/config_test.rs b/framework/tests/functionality/config_test.rs index 414c981..d09c67d 100644 --- a/framework/tests/functionality/config_test.rs +++ b/framework/tests/functionality/config_test.rs @@ -1,9 +1,11 @@ use crate::structures::BaseTestsBuildup; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::config::{config_exists, delete_config, get_configs, upload_config}; use std::path::Path; #[tokio::test] +#[parallel] async fn upload_config_uploads_config() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; let _ = delete_config(&config.context, "UploadConfig").await; @@ -27,6 +29,7 @@ async fn upload_config_uploads_config() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn get_configs_gets_configs() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; let configs = get_configs(&config.context).await.unwrap(); @@ -35,6 +38,7 @@ async fn get_configs_gets_configs() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn delete_config_deletes_config() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; let _ = delete_config(&config.context, "DeleteConfig").await; @@ -58,6 +62,7 @@ async fn delete_config_deletes_config() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn config_exists_works_when_config_exists() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; assert!(config_exists(&config.context, "_default").await.unwrap()); @@ -65,6 +70,7 @@ async fn config_exists_works_when_config_exists() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn config_exists_works_when_config_doesent_exist() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; assert!(!config_exists(&config.context, "_this_does_not_exist") diff --git a/framework/tests/functionality/def_type_test.rs b/framework/tests/functionality/def_type_test.rs index 789ad45..a25311b 100644 --- a/framework/tests/functionality/def_type_test.rs +++ b/framework/tests/functionality/def_type_test.rs @@ -1,10 +1,12 @@ use crate::structures::{get_test_data, FunctionalityTestsBuildup, Population}; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::def_type::{DefType, DismaxQuery, EdismaxQuery, LuceneQuery}; use solrstice::queries::index::UpdateQuery; use solrstice::queries::select::SelectQuery; #[tokio::test] +#[parallel] pub async fn test_dismax_query_parser() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("Dismax").await.unwrap(); let update = UpdateQuery::new(); @@ -30,6 +32,7 @@ pub async fn test_dismax_query_parser() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_edismax_query_parser() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("Edismax") .await @@ -58,6 +61,7 @@ pub async fn test_edismax_query_parser() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_lucene_query_parser() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("Lucene").await.unwrap(); let update = UpdateQuery::new(); diff --git a/framework/tests/functionality/facetset_test.rs b/framework/tests/functionality/facetset_test.rs index 07533f2..0e4d143 100644 --- a/framework/tests/functionality/facetset_test.rs +++ b/framework/tests/functionality/facetset_test.rs @@ -1,4 +1,5 @@ use crate::structures::{get_test_data, FunctionalityTestsBuildup}; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::components::facet_set::{ FacetSetComponent, FieldFacetComponent, FieldFacetEntry, PivotFacetComponent, @@ -7,6 +8,7 @@ use solrstice::queries::index::UpdateQuery; use solrstice::queries::select::SelectQuery; #[tokio::test] +#[parallel] pub async fn test_facet_pivot_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("FacetPivot") .await @@ -45,6 +47,7 @@ pub async fn test_facet_pivot_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_facet_query_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("FacetQuery") .await @@ -69,6 +72,7 @@ pub async fn test_facet_query_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_facet_field_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("FacetField") .await @@ -95,6 +99,7 @@ pub async fn test_facet_field_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_facet_field_exclude_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("FacetFieldExclude") .await @@ -124,6 +129,7 @@ pub async fn test_facet_field_exclude_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_facet_field_exclude_works_missing() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("FacetFieldMissing") .await diff --git a/framework/tests/functionality/grouping_tests.rs b/framework/tests/functionality/grouping_tests.rs index 5c4cd91..91b5d61 100644 --- a/framework/tests/functionality/grouping_tests.rs +++ b/framework/tests/functionality/grouping_tests.rs @@ -1,4 +1,5 @@ use crate::structures::{get_test_data, FunctionalityTestsBuildup}; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::components::grouping::{GroupFormatting, GroupingComponent}; use solrstice::queries::index::UpdateQuery; @@ -6,6 +7,7 @@ use solrstice::queries::select::SelectQuery; use std::collections::HashMap; #[tokio::test] +#[parallel] async fn group_fields() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("GroupBasic") .await @@ -38,6 +40,7 @@ async fn group_fields() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn group_queries() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("GroupQuery") .await @@ -75,6 +78,7 @@ async fn group_queries() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn group_n_groups() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("GroupNGroups") .await @@ -107,6 +111,7 @@ async fn group_n_groups() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn group_main() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("GroupMain") .await @@ -134,6 +139,7 @@ async fn group_main() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn group_main_false() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("GroupMainFalse") .await @@ -159,6 +165,7 @@ async fn group_main_false() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn group_simple() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("GroupSimple") .await diff --git a/framework/tests/functionality/index_test.rs b/framework/tests/functionality/index_test.rs index ba14a60..fecabb5 100644 --- a/framework/tests/functionality/index_test.rs +++ b/framework/tests/functionality/index_test.rs @@ -1,4 +1,5 @@ use crate::structures::{get_test_data, BaseTestsBuildup, City, FunctionalityTestsBuildup}; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::collection::{create_collection, delete_collection}; use solrstice::queries::config::{delete_config, upload_config}; @@ -7,6 +8,7 @@ use solrstice::queries::select::SelectQuery; use std::path::Path; #[tokio::test] +#[parallel] async fn index_indexes_documents() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; let config_name = "IndexConfig".to_string(); @@ -39,6 +41,7 @@ async fn index_indexes_documents() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn index_indexes_correct_documents() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; let config_name = "IndexCorrectConfig".to_string(); @@ -83,6 +86,7 @@ async fn index_indexes_correct_documents() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn delete_deletes_documents_by_id() { let test_data_name = "DeleteDeletesById".to_string(); let config = FunctionalityTestsBuildup::build_up(&test_data_name) diff --git a/framework/tests/functionality/json_facet_test.rs b/framework/tests/functionality/json_facet_test.rs index 9720068..0071da6 100644 --- a/framework/tests/functionality/json_facet_test.rs +++ b/framework/tests/functionality/json_facet_test.rs @@ -1,4 +1,5 @@ use crate::structures::{get_test_data, FunctionalityTestsBuildup}; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::models::json_facet::SolrJsonFacetResponse; use solrstice::queries::components::json_facet::{ @@ -8,6 +9,7 @@ use solrstice::queries::index::UpdateQuery; use solrstice::queries::select::SelectQuery; #[tokio::test] +#[parallel] pub async fn test_json_query_facet_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("JsonFacetQuery") .await @@ -36,6 +38,7 @@ pub async fn test_json_query_facet_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_json_stat_facet_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("JsonFacetStats") .await @@ -64,6 +67,7 @@ pub async fn test_json_stat_facet_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_json_terms_facet_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("JsonFacetTerms") .await @@ -92,6 +96,7 @@ pub async fn test_json_terms_facet_works() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] pub async fn test_json_facet_sub_works() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("JsonFacetSub") .await diff --git a/framework/tests/functionality/logging_test.rs b/framework/tests/functionality/logging_test.rs index 2f4ee16..d72ba47 100644 --- a/framework/tests/functionality/logging_test.rs +++ b/framework/tests/functionality/logging_test.rs @@ -1,13 +1,14 @@ use crate::structures::BaseTestsBuildup; use log::{Metadata, Record}; +use serial_test::serial; use solrstice::clients::async_cloud_client::AsyncSolrCloudClient; use solrstice::models::context::SolrServerContextBuilder; use solrstice::models::error::SolrError; use solrstice::queries::request_builder::LoggingPolicy; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; struct TestLogger { - messages: Arc>>, + pub messages: Arc>>, } impl log::Log for TestLogger { @@ -25,15 +26,21 @@ impl log::Log for TestLogger { fn flush(&self) {} } -#[tokio::test] -async fn logging_logs_message() -> Result<(), SolrError> { +static LOGGER_MESSAGES: OnceLock>>> = OnceLock::new(); + +pub fn init_logger() -> Arc>> { let messages = Arc::new(Mutex::new(Vec::new())); let logger = TestLogger { messages: Arc::clone(&messages), }; log::set_boxed_logger(Box::new(logger)).unwrap(); log::set_max_level(log::LevelFilter::Debug); + messages +} +#[tokio::test] +#[serial] +async fn logging_logs_message() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; let mut context = SolrServerContextBuilder::new(config.host); if config.auth.is_some() { @@ -42,10 +49,15 @@ async fn logging_logs_message() -> Result<(), SolrError> { let context = context.build(); let client = AsyncSolrCloudClient::new(context); - messages.lock().unwrap().clear(); + LOGGER_MESSAGES + .get_or_init(init_logger) + .lock() + .unwrap() + .clear(); let _ = client.get_configs().await.unwrap(); - for message in &*messages.lock().unwrap() { + let messages = LOGGER_MESSAGES.get().unwrap().lock().unwrap(); + for message in messages.iter() { if message.contains("Sending Solr request to") { return Ok(()); } @@ -54,14 +66,8 @@ async fn logging_logs_message() -> Result<(), SolrError> { } #[tokio::test] +#[serial] async fn logging_does_not_log_message_if_disabled() -> Result<(), SolrError> { - let messages = Arc::new(Mutex::new(Vec::new())); - let logger = TestLogger { - messages: Arc::clone(&messages), - }; - log::set_boxed_logger(Box::new(logger)).unwrap(); - log::set_max_level(log::LevelFilter::Debug); - let config = BaseTestsBuildup::new().await; let mut context = SolrServerContextBuilder::new(config.host).with_logging_policy(LoggingPolicy::Off); @@ -71,10 +77,16 @@ async fn logging_does_not_log_message_if_disabled() -> Result<(), SolrError> { let context = context.build(); let client = AsyncSolrCloudClient::new(context); - messages.lock().unwrap().clear(); + LOGGER_MESSAGES + .get_or_init(init_logger) + .lock() + .unwrap() + .clear(); let _ = client.get_configs().await.unwrap(); - for message in &*messages.lock().unwrap() { + + let messages = LOGGER_MESSAGES.get().unwrap().lock().unwrap(); + for message in messages.iter() { if message.contains("Sending Solr request to") { return Err(SolrError::Unknown(format!( "Did not expect log message, but got {}", diff --git a/framework/tests/functionality/readme_test.rs b/framework/tests/functionality/readme_test.rs index b49c083..3e684dd 100644 --- a/framework/tests/functionality/readme_test.rs +++ b/framework/tests/functionality/readme_test.rs @@ -4,6 +4,7 @@ use solrstice::clients::async_cloud_client::AsyncSolrCloudClient; // use solrstice::hosts::solr_server_host::SolrSingleServerHost; // use solrstice::models::auth::SolrBasicAuth; // use solrstice::models::context::SolrServerContextBuilder; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::index::{DeleteQuery, UpdateQuery}; use solrstice::queries::select::SelectQuery; @@ -15,6 +16,7 @@ struct TestData { } #[tokio::test] +#[parallel] pub async fn example() -> Result<(), SolrError> { let config = BaseTestsBuildup::new().await; diff --git a/framework/tests/functionality/select_test.rs b/framework/tests/functionality/select_test.rs index bcf118f..2b518e5 100644 --- a/framework/tests/functionality/select_test.rs +++ b/framework/tests/functionality/select_test.rs @@ -1,9 +1,11 @@ use crate::structures::{get_test_data, City, FunctionalityTestsBuildup}; +use serial_test::parallel; use solrstice::models::error::SolrError; use solrstice::queries::index::UpdateQuery; use solrstice::queries::select::SelectQuery; #[tokio::test] +#[parallel] async fn select_works_when_no_result() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("SelectNoResult") .await @@ -27,6 +29,7 @@ async fn select_works_when_no_result() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn select_works_when_no_result_serde_value() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("SelectNoResultSerdeValue") .await @@ -50,6 +53,7 @@ async fn select_works_when_no_result_serde_value() -> Result<(), SolrError> { } #[tokio::test] +#[parallel] async fn select_works_using_cursor_mark() -> Result<(), SolrError> { let config = FunctionalityTestsBuildup::build_up("SelectCursorMark") .await diff --git a/framework/tests/functionality/zk_test.rs b/framework/tests/functionality/zk_test.rs index 1820099..003dd2a 100644 --- a/framework/tests/functionality/zk_test.rs +++ b/framework/tests/functionality/zk_test.rs @@ -1,10 +1,12 @@ use crate::structures::BaseTestsBuildup; +use serial_test::parallel; use solrstice::hosts::solr_host::SolrHost; use solrstice::hosts::zookeeper_host::ZookeeperEnsembleHostConnector; use std::time::Duration; use std::vec; #[tokio::test] +#[parallel] async fn create_zookeeper_client() { BaseTestsBuildup::new().await; let zk_hosts = vec![std::env::var("ZK_HOST").unwrap()]; @@ -15,6 +17,7 @@ async fn create_zookeeper_client() { } #[tokio::test] +#[parallel] async fn get_solr_node_from_zookeeper() { BaseTestsBuildup::new().await; let zk_hosts = vec![std::env::var("ZK_HOST").unwrap()]; From 7ea612a5b01ed9ce820e374268a9d01b3675aa67 Mon Sep 17 00:00:00 2001 From: Sh1nku <42642351+Sh1nku@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:48:38 +0200 Subject: [PATCH 5/9] Add logging_policy implementation to python --- wrappers/python/requirements-dev.txt | 1 + wrappers/python/solrstice/clients.pyi | 12 +--- wrappers/python/solrstice/hosts.pyi | 42 +++++++++++++- wrappers/python/src/hosts.rs | 10 +++- wrappers/python/src/models/context.rs | 82 ++++++++++++++++++++++++++- wrappers/python/tests/helpers.py | 6 +- wrappers/python/tests/test_clients.py | 11 ++-- wrappers/python/tests/test_hosts.py | 7 +++ wrappers/python/tests/test_logging.py | 53 +++++++++++------ 9 files changed, 183 insertions(+), 41 deletions(-) diff --git a/wrappers/python/requirements-dev.txt b/wrappers/python/requirements-dev.txt index 037f3fa..ffcd913 100644 --- a/wrappers/python/requirements-dev.txt +++ b/wrappers/python/requirements-dev.txt @@ -1,5 +1,6 @@ pytest pytest-asyncio +pytest-xdist[psutil] python-dotenv maturin dataclasses-json diff --git a/wrappers/python/solrstice/clients.pyi b/wrappers/python/solrstice/clients.pyi index 5bc0abd..aeedc55 100644 --- a/wrappers/python/solrstice/clients.pyi +++ b/wrappers/python/solrstice/clients.pyi @@ -24,10 +24,7 @@ class AsyncSolrCloudClient: """ pass async def get_configs(self) -> List[str]: - """Gets a list of Solr configs on a Solr instance - - :param context: SolrServerRequest context - """ + """Gets a list of Solr configs on a Solr instance""" pass async def config_exists(self, config_name: str) -> bool: @@ -75,8 +72,6 @@ class AsyncSolrCloudClient: async def delete_collection(self, name: str) -> None: """ Delete a config from the Solr server. - - :param context: The Solr server context. :param name: The name of the collection to delete. """ async def create_alias(self, name: str, collections: List[str]) -> None: @@ -144,10 +139,7 @@ class BlockingSolrCloudClient: """ pass def get_configs(self) -> List[str]: - """Gets a list of Solr configs on a Solr instance - - :param context: SolrServerRequest context - """ + """Gets a list of Solr configs on a Solr instance""" pass def config_exists(self, config_name: str) -> bool: diff --git a/wrappers/python/solrstice/hosts.pyi b/wrappers/python/solrstice/hosts.pyi index ff2d9fe..0374add 100644 --- a/wrappers/python/solrstice/hosts.pyi +++ b/wrappers/python/solrstice/hosts.pyi @@ -1,5 +1,6 @@ +import abc from abc import ABC -from typing import List, Optional +from typing import List, Optional, Union from solrstice.auth import SolrAuth @@ -44,12 +45,47 @@ class ZookeeperEnsembleHostConnector: """Connect to the Zookeeper ensemble""" pass +class LoggingPolicy(abc.ABC): + """Policy describing how to log solr queries. Valid values are :class:`OffLoggingPolicy`, :class:`FastLoggingPolicy`, and :class:`PrettyLoggingPolicy`""" + + pass + +class OffLoggingPolicy(LoggingPolicy): + """Do not log requests""" + + def __init__(self): + pass + +class FastLoggingPolicy(LoggingPolicy): + """For each request create a logging::DEBUG message with url, headers, and body + + :param max_body_length: How long to allow the body to be before dropping to log it + """ + + def __init__(self, max_body_length: int) -> None: + pass + +class PrettyLoggingPolicy(LoggingPolicy): + """For each request create a logging::DEBUG message with url, headers, and a pretty body + + :param max_body_length: How long to allow the body to be before dropping to log it + """ + + def __init__(self, max_body_length: int) -> None: + pass + class SolrServerContext: """The context for a connection to a solr instance - :param host: An instance of SolrHost specifying how to connect to a solr instance + :param host: An instance of SolrHost specifying how to connect to a solr instance. If given as a string it creates a :class:`SolrSingleServerHost` :param auth: An instance of SolrAuth specifying how to authenticate with the solr instance + :param logging_policy: How to log solr queries, valid values are :class:`OffLoggingPolicy`, :class:`FastLoggingPolicy`, and :class:`PrettyLoggingPolicy` """ - def __init__(self, host: SolrHost, auth: Optional[SolrAuth] = None): + def __init__( + self, + host: Union[SolrHost, str], + auth: Optional[SolrAuth] = None, + logging_policy: Optional[LoggingPolicy] = None, + ): pass diff --git a/wrappers/python/src/hosts.rs b/wrappers/python/src/hosts.rs index 540b20e..615aae0 100644 --- a/wrappers/python/src/hosts.rs +++ b/wrappers/python/src/hosts.rs @@ -1,4 +1,7 @@ -use crate::models::context::SolrServerContextWrapper; +use crate::models::context::{ + FastLoggingPolicyWrapper, LoggingPolicyWrapper, OffLoggingPolicyWrapper, + PrettyLoggingPolicyWrapper, SolrServerContextWrapper, +}; use crate::models::error::PyErrWrapper; use async_trait::async_trait; use pyo3::prelude::*; @@ -18,6 +21,11 @@ pub fn hosts(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/wrappers/python/src/models/context.rs b/wrappers/python/src/models/context.rs index d764b33..7e6a1f6 100644 --- a/wrappers/python/src/models/context.rs +++ b/wrappers/python/src/models/context.rs @@ -1,7 +1,15 @@ -use crate::hosts::SolrHostWrapper; +use crate::hosts::{SolrHostWrapper, SolrSingleServerHostWrapper}; use crate::models::auth::SolrAuthWrapper; use pyo3::prelude::*; +use serde::{Deserialize, Serialize}; use solrstice::models::context::{SolrServerContext, SolrServerContextBuilder}; +use solrstice::queries::request_builder::LoggingPolicy; + +#[derive(FromPyObject)] +pub enum SolrHostUnion { + SolrHostWrapperEnumValue(SolrHostWrapper), + String(String), +} #[pyclass(name = "SolrServerContext", module = "solrstice.hosts", subclass)] #[derive(Clone)] @@ -10,12 +18,24 @@ pub struct SolrServerContextWrapper(SolrServerContext); #[pymethods] impl SolrServerContextWrapper { #[new] - pub fn new(host: SolrHostWrapper, auth: Option) -> Self { + pub fn new( + host: SolrHostUnion, + auth: Option, + logging_policy: Option, + ) -> Self { + let host = match host { + SolrHostUnion::SolrHostWrapperEnumValue(h) => h, + SolrHostUnion::String(s) => SolrSingleServerHostWrapper::new(s).1, + }; let mut builder = SolrServerContextBuilder::new(host); builder = match auth { Some(auth) => builder.with_auth(auth), None => builder, }; + builder = match logging_policy { + Some(logging_policy) => builder.with_logging_policy(logging_policy.into()), + None => builder, + }; SolrServerContextWrapper(builder.build()) } } @@ -31,3 +51,61 @@ impl<'a> From<&'a SolrServerContextWrapper> for &'a SolrServerContext { &value.0 } } + +#[pyclass(name = "LoggingPolicy", module = "solrstice.hosts", subclass)] +#[derive(Clone)] +pub struct LoggingPolicyWrapper(LoggingPolicy); + +impl From for LoggingPolicy { + fn from(value: LoggingPolicyWrapper) -> Self { + value.0 + } +} + +impl From for LoggingPolicyWrapper { + fn from(value: LoggingPolicy) -> Self { + Self(value) + } +} + +#[pyclass(name = "OffLoggingPolicy", extends=LoggingPolicyWrapper, module = "solrstice.hosts", subclass)] +#[derive(Clone, Serialize, Deserialize)] +pub struct OffLoggingPolicyWrapper {} + +#[pymethods] +impl OffLoggingPolicyWrapper { + #[new] + pub fn new() -> (Self, LoggingPolicyWrapper) { + (Self {}, LoggingPolicyWrapper(LoggingPolicy::Off)) + } +} + +#[pyclass(name = "FastLoggingPolicy", extends=LoggingPolicyWrapper, module = "solrstice.hosts", subclass)] +#[derive(Clone, Serialize, Deserialize)] +pub struct FastLoggingPolicyWrapper {} + +#[pymethods] +impl FastLoggingPolicyWrapper { + #[new] + pub fn new(max_body_length: usize) -> (Self, LoggingPolicyWrapper) { + ( + Self {}, + LoggingPolicyWrapper(LoggingPolicy::Fast(max_body_length)), + ) + } +} + +#[pyclass(name = "PrettyLoggingPolicy", extends=LoggingPolicyWrapper, module = "solrstice.hosts", subclass)] +#[derive(Clone, Serialize, Deserialize)] +pub struct PrettyLoggingPolicyWrapper {} + +#[pymethods] +impl PrettyLoggingPolicyWrapper { + #[new] + pub fn new(max_body_length: usize) -> (Self, LoggingPolicyWrapper) { + ( + Self {}, + LoggingPolicyWrapper(LoggingPolicy::Pretty(max_body_length)), + ) + } +} diff --git a/wrappers/python/tests/helpers.py b/wrappers/python/tests/helpers.py index b8806da..2430c0f 100644 --- a/wrappers/python/tests/helpers.py +++ b/wrappers/python/tests/helpers.py @@ -22,6 +22,7 @@ class Config: speedbump_host: Optional[str] solr_username: Optional[str] solr_password: Optional[str] + solr_auth: Optional[SolrBasicAuth] context: SolrServerContext config_path: str async_client: AsyncSolrCloudClient @@ -52,6 +53,7 @@ def create_config() -> Config: speedbump_host, solr_username, solr_password, + solr_auth, context, os.path.join(get_path_prefix(), "test_setup/test_collection"), AsyncSolrCloudClient(context), @@ -63,7 +65,7 @@ def wait_for_solr(host: str, max_time: int): while time.time() < end: try: with urlopen( - f'{host}{"/solr/admin/collections"}?action=CLUSTERSTATUS' + f'{host}{"/solr/admin/collections"}?action=CLUSTERSTATUS' ) as response: if response.status == 200: return @@ -105,7 +107,7 @@ async def index_test_data(context: SolrServerContext, name: str) -> None: async def setup_collection( - context: SolrServerContext, name: str, config_path: str + context: SolrServerContext, name: str, config_path: str ) -> None: try: await delete_collection(context, name) diff --git a/wrappers/python/tests/test_clients.py b/wrappers/python/tests/test_clients.py index 3806dc7..f3e074b 100644 --- a/wrappers/python/tests/test_clients.py +++ b/wrappers/python/tests/test_clients.py @@ -1,11 +1,10 @@ import asyncio import pytest +from helpers import Config, create_config from typing_extensions import Optional -from helpers import Config, create_config from solrstice.auth import SolrAuth, SolrBasicAuth - from solrstice.clients import AsyncSolrCloudClient, BlockingSolrCloudClient from solrstice.hosts import SolrServerContext, SolrSingleServerHost from solrstice.queries import DeleteQuery, SelectQuery, UpdateQuery @@ -107,13 +106,15 @@ def __new__(cls, host: str, auth: Optional[SolrAuth] = None): return super().__new__(cls, context=context) def test_method(self) -> str: - return 'test' + return "test" name = "SubclassingClientWorks" config = create_config() - client = SolrClient(config.solr_host, SolrBasicAuth(config.solr_username, config.solr_password)) + client = SolrClient( + config.solr_host, SolrBasicAuth(config.solr_username, config.solr_password) + ) try: await client.delete_config(name) @@ -121,6 +122,6 @@ def test_method(self) -> str: pass await client.upload_config(name, config.config_path) - assert client.test_method() == 'test' + assert client.test_method() == "test" await client.delete_config(name) diff --git a/wrappers/python/tests/test_hosts.py b/wrappers/python/tests/test_hosts.py index a9dabed..7d56943 100644 --- a/wrappers/python/tests/test_hosts.py +++ b/wrappers/python/tests/test_hosts.py @@ -65,6 +65,13 @@ async def test_solr_single_server_works(config: Config): await get_configs(context) +@pytest.mark.asyncio +async def test_solr_single_server_works_with_string(config: Config): + wait_for_solr(config.host, 30) + context = SolrServerContext(config.host, config.auth) + await get_configs(context) + + @pytest.mark.asyncio async def test_multiple_server_works(config: Config): wait_for_solr(config.host, 30) diff --git a/wrappers/python/tests/test_logging.py b/wrappers/python/tests/test_logging.py index 108fac2..c8dd6cc 100644 --- a/wrappers/python/tests/test_logging.py +++ b/wrappers/python/tests/test_logging.py @@ -2,9 +2,10 @@ import pytest from _pytest.logging import LogCaptureFixture +from helpers import Config, create_config, wait_for_solr -from helpers import Config, create_config, wait_for_solr, setup_collection, index_test_data, teardown_collection -from solrstice.queries import SelectQuery +from solrstice.clients import AsyncSolrCloudClient +from solrstice.hosts import OffLoggingPolicy, SolrServerContext @pytest.fixture() @@ -12,22 +13,38 @@ def config() -> Config: yield create_config() -@pytest.mark.asyncio -async def test_sending_select_query_writes_message(config: Config, caplog: LogCaptureFixture): - caplog.set_level(logging.DEBUG) - name = "SendingSelectQueryWritesMessage" - wait_for_solr(config.solr_host, 30) - - try: - await setup_collection(config.context, name, config.config_path) +# TODO This test fails if run in parallel with the rest of the test suite, but not if run alone +# @pytest.mark.asyncio +# async def test_logging_logs_message(config: Config, caplog: LogCaptureFixture): +# wait_for_solr(config.solr_host, 30) +# +# with caplog.at_level(logging.DEBUG): +# caplog.clear() +# assert not any( +# "Sending Solr request to" in msg for msg in [x.getMessage() for x in caplog.records]), "Logs are not empty" +# await config.async_client.get_configs() +# assert any( +# "Sending Solr request to" in msg for msg in +# [x.getMessage() for x in caplog.records]), "Expected log message not found" - await index_test_data(config.context, name) - builder = SelectQuery() - await builder.execute(config.context, name) - - for record in caplog.records: - print(record) +@pytest.mark.asyncio +async def test_logging_does_not_log_message_if_disabled( + config: Config, caplog: LogCaptureFixture +): + wait_for_solr(config.solr_host, 30) - finally: - await teardown_collection(config.context, name) + context = SolrServerContext(config.solr_host, config.solr_auth, OffLoggingPolicy()) + client = AsyncSolrCloudClient(context) + + with caplog.at_level(logging.DEBUG): + caplog.clear() + assert not any( + "Sending Solr request to" in msg + for msg in [x.getMessage() for x in caplog.records] + ), "Logs are not empty" + await client.get_configs() + assert not any( + "Sending Solr request to" in msg + for msg in [x.getMessage() for x in caplog.records] + ), "Logs are not empty" From 95ec48c4e7d76a8ea424cbb211f37a37bcd93680 Mon Sep 17 00:00:00 2001 From: Sh1nku <42642351+Sh1nku@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:55:01 +0200 Subject: [PATCH 6/9] Change to not use serde from_utf8_lossy, but rather String::from_utf8_lossy in logging --- CHANGELOG.md | 18 ++++++++++++++++-- framework/src/queries/request_builder.rs | 3 +-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9bf375..ceb3c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,50 @@ +# v0.5.0 + +* Add logging of solr requests + # v0.4.3 + * Fix mypy not recognizing .pyi files * Add static type checking test for mypy and pyright # v0.4.2 + * Switch out openssl for rustls * Run publish CI when creating PRs # v0.4.1 -* Relax version requirements. + +* Relax version requirements. * Add Python 3.12 to CI * Note: Not released to PyPi due to relying on openssl which could not run in manylinux # v0.4.0 + * Make authentication error into its own error, instead of Json decode error * Make inherited error types transparently pass through parent error # v0.3.2 + * `num_found_exact` was introduced in Solr 8.6. This caused deserialization to fail on older versions. Changed so that it will be emulated as `true` for older versions. # v0.3.1 + * Fix error in python documentation # v0.3.0 + * Add Facet sets * Add Json facets -* Be more permissive with arguments to builders, using `Into