Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "meta-memcache-socket"
version = "0.2.0"
version = "0.3.0"
edition = "2024"

[lib]
Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ meta-memcache-socket-py/
│ ├── lib.rs # PyO3 module entry — exports classes, functions, constants
│ ├── constants.rs # Protocol constants (response codes, set modes, NOOP, ENDL)
│ ├── memcache_socket.rs # MemcacheSocket class — socket I/O, buffering, GIL management
│ ├── request_flags.rs # RequestFlags class — mutable flags for building commands
│ ├── request_flags.rs # RequestFlags class — immutable flags for building commands
│ ├── response_flags.rs # ResponseFlags class — immutable flags parsed from responses
│ ├── response_types.rs # Response type classes (Value, Success, Miss, NotStored, Conflict)
│ ├── impl_build_cmd.rs # Command builder — key validation, base64, flag encoding
Expand Down Expand Up @@ -175,7 +175,7 @@ flags.opaque # Optional[bytes] — echoed opaque data (O)

### RequestFlags

Mutable container for flags sent with commands.
Immutable container for flags sent with commands.

```python
from meta_memcache_socket import RequestFlags
Expand Down Expand Up @@ -205,11 +205,28 @@ flags = RequestFlags(
opaque=None, # O — opaque data echoed back
mode=None, # M — operation mode (set/arithmetic)
)
```

The flags are immutable, so they can be reused safely across threads when
calling meta commands. Internal layers migth need to mutate flags
(content id, reduce ttl, etc...) and will mutate them use replace() to create
modified copies when needed.

If you need to change flags on a existing RequestFlags, use the `replace()` method:

flags.copy() # -> RequestFlags (deep copy)
```python
new_flags = flags.replace(return_ttl=True, cache_ttl=600) # -> RequestFlags
```

You can also encode the flags into a byte string for command building, showing
exactly what will be sent on the wire:

```python
flags.to_bytes() # -> bytes (encoded flag string)
```

For debugging purposes, stringifying it shows the flags in a human-readable format.

### Command builders

Convenience functions that build meta-protocol command byte strings.
Expand Down
67 changes: 45 additions & 22 deletions meta_memcache_socket.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import socket
from typing import Any, Optional, Tuple, Union
from typing import Any, Final, Optional, Tuple, Union

RESPONSE_VALUE: int # 1 - VALUE (VA)
RESPONSE_SUCCESS: int # 2 - SUCCESS (OK or HD)
Expand Down Expand Up @@ -56,26 +56,26 @@ class RequestFlags:
* mode: The mode to use when storing the value in the cache. See SET_MODE_* and MA_MODE_* constants
"""

no_reply: bool
return_client_flag: bool
return_cas_token: bool
return_value: bool
return_ttl: bool
return_size: bool
return_last_access: bool
return_fetched: bool
return_key: bool
no_update_lru: bool
mark_stale: bool
cache_ttl: Optional[int]
recache_ttl: Optional[int]
vivify_on_miss_ttl: Optional[int]
client_flag: Optional[int]
ma_initial_value: Optional[int]
ma_delta_value: Optional[int]
cas_token: Optional[int]
opaque: Optional[bytes]
mode: Optional[int]
no_reply: Final[bool]
return_client_flag: Final[bool]
return_cas_token: Final[bool]
return_value: Final[bool]
return_ttl: Final[bool]
return_size: Final[bool]
return_last_access: Final[bool]
return_fetched: Final[bool]
return_key: Final[bool]
no_update_lru: Final[bool]
mark_stale: Final[bool]
cache_ttl: Final[Optional[int]]
recache_ttl: Final[Optional[int]]
vivify_on_miss_ttl: Final[Optional[int]]
client_flag: Final[Optional[int]]
ma_initial_value: Final[Optional[int]]
ma_delta_value: Final[Optional[int]]
cas_token: Final[Optional[int]]
opaque: Final[Optional[bytes]]
mode: Final[Optional[int]]

def __init__(
self,
Expand All @@ -101,7 +101,30 @@ class RequestFlags:
opaque: Optional[bytes] = None,
mode: Optional[int] = None,
) -> None: ...
def copy(self) -> "RequestFlags": ...
def replace(
self,
*,
no_reply: Optional[bool] = None,
return_client_flag: Optional[bool] = None,
return_cas_token: Optional[bool] = None,
return_value: Optional[bool] = None,
return_ttl: Optional[bool] = None,
return_size: Optional[bool] = None,
return_last_access: Optional[bool] = None,
return_fetched: Optional[bool] = None,
return_key: Optional[bool] = None,
no_update_lru: Optional[bool] = None,
mark_stale: Optional[bool] = None,
cache_ttl: Optional[int] = None,
recache_ttl: Optional[int] = None,
vivify_on_miss_ttl: Optional[int] = None,
client_flag: Optional[int] = None,
ma_initial_value: Optional[int] = None,
ma_delta_value: Optional[int] = None,
cas_token: Optional[int] = None,
opaque: Optional[bytes] = None,
mode: Optional[int] = None,
) -> "RequestFlags": ...
def to_bytes(self) -> bytes: ...
def __str__(self) -> str: ...

Expand Down
124 changes: 101 additions & 23 deletions src/request_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,48 @@ use pyo3::types::PyBytes;

use crate::{MA_MODE_INC, SET_MODE_SET};

#[pyclass(eq, skip_from_py_object)]
#[pyclass(eq, skip_from_py_object, frozen)]
#[derive(Clone, Debug, PartialEq)]
pub struct RequestFlags {
#[pyo3(get, set)]
#[pyo3(get)]
no_reply: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_client_flag: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_cas_token: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_value: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_ttl: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_size: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_last_access: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_fetched: bool,
#[pyo3(get, set)]
#[pyo3(get)]
return_key: bool,
#[pyo3(get, set)]
#[pyo3(get)]
no_update_lru: bool,
#[pyo3(get, set)]
#[pyo3(get)]
mark_stale: bool,
#[pyo3(get, set)]
#[pyo3(get)]
cache_ttl: Option<u32>,
#[pyo3(get, set)]
#[pyo3(get)]
recache_ttl: Option<u32>,
#[pyo3(get, set)]
#[pyo3(get)]
vivify_on_miss_ttl: Option<u32>,
#[pyo3(get, set)]
#[pyo3(get)]
client_flag: Option<u32>,
#[pyo3(get, set)]
#[pyo3(get)]
ma_initial_value: Option<u64>,
#[pyo3(get, set)]
#[pyo3(get)]
ma_delta_value: Option<u64>,
#[pyo3(get, set)]
#[pyo3(get)]
cas_token: Option<u32>,
#[pyo3(get, set)]
#[pyo3(get)]
opaque: Option<Vec<u8>>,
#[pyo3(get, set)]
#[pyo3(get)]
mode: Option<u8>,
}

Expand Down Expand Up @@ -251,8 +251,86 @@ impl RequestFlags {
}
}

pub fn copy(&self) -> Self {
self.clone()
/// Return a copy of this object with the specified fields replaced.
///
/// Only keyword arguments that are explicitly provided (non-None) override the
/// corresponding field. Fields not mentioned keep their current value.
///
/// Note: passing `None` explicitly for an optional field (e.g. `cache_ttl=None`)
/// keeps the existing value rather than unsetting it. To unset an optional field,
/// construct a new `RequestFlags` directly.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This could be confusing, but for common use cases (e.g., ttl override), it should be fine.

#[allow(clippy::too_many_arguments)]
#[pyo3(
signature = (
/,
*,
no_reply=None,
return_client_flag=None,
return_cas_token=None,
return_value=None,
return_ttl=None,
return_size=None,
return_last_access=None,
return_fetched=None,
return_key=None,
no_update_lru=None,
mark_stale=None,
cache_ttl=None,
recache_ttl=None,
vivify_on_miss_ttl=None,
client_flag=None,
ma_initial_value=None,
ma_delta_value=None,
cas_token=None,
opaque=None,
mode=None
)
)]
pub fn replace(
&self,
no_reply: Option<bool>,
return_client_flag: Option<bool>,
return_cas_token: Option<bool>,
return_value: Option<bool>,
return_ttl: Option<bool>,
return_size: Option<bool>,
return_last_access: Option<bool>,
return_fetched: Option<bool>,
return_key: Option<bool>,
no_update_lru: Option<bool>,
mark_stale: Option<bool>,
cache_ttl: Option<u32>,
recache_ttl: Option<u32>,
vivify_on_miss_ttl: Option<u32>,
client_flag: Option<u32>,
ma_initial_value: Option<u64>,
ma_delta_value: Option<u64>,
cas_token: Option<u32>,
opaque: Option<Vec<u8>>,
mode: Option<u8>,
) -> Self {
RequestFlags {
no_reply: no_reply.unwrap_or(self.no_reply),
return_client_flag: return_client_flag.unwrap_or(self.return_client_flag),
return_cas_token: return_cas_token.unwrap_or(self.return_cas_token),
return_value: return_value.unwrap_or(self.return_value),
return_ttl: return_ttl.unwrap_or(self.return_ttl),
return_size: return_size.unwrap_or(self.return_size),
return_last_access: return_last_access.unwrap_or(self.return_last_access),
return_fetched: return_fetched.unwrap_or(self.return_fetched),
return_key: return_key.unwrap_or(self.return_key),
no_update_lru: no_update_lru.unwrap_or(self.no_update_lru),
mark_stale: mark_stale.unwrap_or(self.mark_stale),
cache_ttl: cache_ttl.or(self.cache_ttl),
recache_ttl: recache_ttl.or(self.recache_ttl),
vivify_on_miss_ttl: vivify_on_miss_ttl.or(self.vivify_on_miss_ttl),
client_flag: client_flag.or(self.client_flag),
ma_initial_value: ma_initial_value.or(self.ma_initial_value),
ma_delta_value: ma_delta_value.or(self.ma_delta_value),
cas_token: cas_token.or(self.cas_token),
opaque: opaque.or_else(|| self.opaque.clone()),
mode: mode.or(self.mode),
}
}

pub fn __str__(&self) -> String {
Expand Down
Loading
Loading