maskflow-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .env
6
+ node_modules/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .DS_Store
11
+ .pytest_cache/
12
+ .idea/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: maskflow-sdk
3
+ Version: 0.1.0
4
+ Summary: Mask PII before it reaches an LLM, unmask the response. Works with any provider.
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: maskflow-core<0.2,>=0.1.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8.0; extra == 'dev'
10
+ Description-Content-Type: text/markdown
11
+
12
+ # maskflow
13
+
14
+ Mask PII before it reaches an LLM. Unmask the response. Works with any provider.
15
+
16
+ ```python
17
+ from maskflow import mask_and_call
18
+
19
+ def call_claude(masked_prompt: str) -> str:
20
+ return anthropic_client.messages.create(
21
+ model="claude-sonnet-5",
22
+ max_tokens=1024,
23
+ messages=[{"role": "user", "content": masked_prompt}],
24
+ ).content[0].text
25
+
26
+ response = mask_and_call(
27
+ "Hi, I'm Jane Doe (jane@example.com). My order shipped to 123 Main St but never arrived.",
28
+ call_claude,
29
+ )
30
+ # Claude only ever sees "Hi, I'm <PERSON_NAME_1> (<EMAIL_1>). My order shipped to
31
+ # <ADDRESS_1> but never arrived." -- response comes back with the real values restored.
32
+ ```
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install maskflow-sdk
38
+ ```
39
+
40
+ ## Why this shape
41
+
42
+ `mask_and_call` takes a plain function, not a specific provider's client. You write the one line
43
+ that actually calls your LLM (Claude, OpenAI, Gemini, a local model, anything) -- maskflow never
44
+ parses or depends on any provider's SDK, so it doesn't break when a provider changes their API and
45
+ works with providers it's never heard of.
46
+
47
+ ```python
48
+ response = mask_and_call(prompt, lambda masked: my_llm_client.generate(masked))
49
+ ```
50
+
51
+ ## Lower-level API
52
+
53
+ For more control than the wrapper gives, `mask`/`unmask` are available directly and are pure,
54
+ stateless functions -- no files, no database. Persisting the mapping between calls is your
55
+ responsibility.
56
+
57
+ ```python
58
+ from maskflow import mask, unmask
59
+
60
+ result = mask("Email me at alice@example.com.")
61
+ result.masked_text # "Email me at <EMAIL_1>."
62
+ result.mapping # {"<EMAIL_1>": "alice@example.com"}
63
+
64
+ unmask(result.masked_text, result.mapping) # original text, restored
65
+ ```
66
+
67
+ ## What gets detected
68
+
69
+ Email, phone, SSN, credit card, IP address, AWS access key, API key / generic secret, JWT, IBAN,
70
+ street address, person name, date of birth -- via regex + structural validation (Luhn, mod-97,
71
+ etc.) plus spaCy NER for names and dates, with keyword-context confidence boosting. See
72
+ [`maskflow-core`](../../core) for detection internals.
73
+
74
+ ## Tests
75
+
76
+ ```bash
77
+ uv run pytest
78
+ ```
@@ -0,0 +1,67 @@
1
+ # maskflow
2
+
3
+ Mask PII before it reaches an LLM. Unmask the response. Works with any provider.
4
+
5
+ ```python
6
+ from maskflow import mask_and_call
7
+
8
+ def call_claude(masked_prompt: str) -> str:
9
+ return anthropic_client.messages.create(
10
+ model="claude-sonnet-5",
11
+ max_tokens=1024,
12
+ messages=[{"role": "user", "content": masked_prompt}],
13
+ ).content[0].text
14
+
15
+ response = mask_and_call(
16
+ "Hi, I'm Jane Doe (jane@example.com). My order shipped to 123 Main St but never arrived.",
17
+ call_claude,
18
+ )
19
+ # Claude only ever sees "Hi, I'm <PERSON_NAME_1> (<EMAIL_1>). My order shipped to
20
+ # <ADDRESS_1> but never arrived." -- response comes back with the real values restored.
21
+ ```
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install maskflow-sdk
27
+ ```
28
+
29
+ ## Why this shape
30
+
31
+ `mask_and_call` takes a plain function, not a specific provider's client. You write the one line
32
+ that actually calls your LLM (Claude, OpenAI, Gemini, a local model, anything) -- maskflow never
33
+ parses or depends on any provider's SDK, so it doesn't break when a provider changes their API and
34
+ works with providers it's never heard of.
35
+
36
+ ```python
37
+ response = mask_and_call(prompt, lambda masked: my_llm_client.generate(masked))
38
+ ```
39
+
40
+ ## Lower-level API
41
+
42
+ For more control than the wrapper gives, `mask`/`unmask` are available directly and are pure,
43
+ stateless functions -- no files, no database. Persisting the mapping between calls is your
44
+ responsibility.
45
+
46
+ ```python
47
+ from maskflow import mask, unmask
48
+
49
+ result = mask("Email me at alice@example.com.")
50
+ result.masked_text # "Email me at <EMAIL_1>."
51
+ result.mapping # {"<EMAIL_1>": "alice@example.com"}
52
+
53
+ unmask(result.masked_text, result.mapping) # original text, restored
54
+ ```
55
+
56
+ ## What gets detected
57
+
58
+ Email, phone, SSN, credit card, IP address, AWS access key, API key / generic secret, JWT, IBAN,
59
+ street address, person name, date of birth -- via regex + structural validation (Luhn, mod-97,
60
+ etc.) plus spaCy NER for names and dates, with keyword-context confidence boosting. See
61
+ [`maskflow-core`](../../core) for detection internals.
62
+
63
+ ## Tests
64
+
65
+ ```bash
66
+ uv run pytest
67
+ ```
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "maskflow-sdk"
3
+ version = "0.1.0"
4
+ description = "Mask PII before it reaches an LLM, unmask the response. Works with any provider."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = { text = "MIT" }
8
+ dependencies = [
9
+ "maskflow-core>=0.1.0,<0.2",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "pytest>=8.0",
15
+ ]
16
+
17
+ [tool.uv.sources]
18
+ maskflow-core = { path = "../../core", editable = true }
19
+
20
+ [tool.uv]
21
+ package = true
22
+
23
+ [build-system]
24
+ requires = ["hatchling"]
25
+ build-backend = "hatchling.build"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/maskflow"]
@@ -0,0 +1,13 @@
1
+ from maskflow_core import Finding, MaskResult, PIIType, detect, mask, unmask
2
+
3
+ from .sdk import mask_and_call
4
+
5
+ __all__ = [
6
+ "detect",
7
+ "mask",
8
+ "unmask",
9
+ "mask_and_call",
10
+ "Finding",
11
+ "PIIType",
12
+ "MaskResult",
13
+ ]
@@ -0,0 +1,23 @@
1
+ from typing import Callable
2
+
3
+ from maskflow_core import mask, unmask
4
+ from maskflow_core.detection import DEFAULT_MIN_CONFIDENCE
5
+
6
+
7
+ def mask_and_call(
8
+ prompt: str,
9
+ call_fn: Callable[[str], str],
10
+ min_confidence: float = DEFAULT_MIN_CONFIDENCE,
11
+ ) -> str:
12
+ """Mask PII in `prompt`, pass the masked text to `call_fn`, and restore the
13
+ original values in whatever `call_fn` returns.
14
+
15
+ `call_fn` is any function that takes a string and returns a string --
16
+ typically a closure around an LLM provider's client call. This makes
17
+ mask_and_call provider-agnostic: it works with Claude, OpenAI, Gemini, a
18
+ local model, or anything else, without MaskFlow depending on any
19
+ provider's SDK.
20
+ """
21
+ result = mask(prompt, min_confidence=min_confidence)
22
+ response = call_fn(result.masked_text)
23
+ return unmask(response, result.mapping)
@@ -0,0 +1,51 @@
1
+ from maskflow import Finding, MaskResult, PIIType, detect, mask, mask_and_call, unmask
2
+
3
+
4
+ def test_mask_and_call_never_exposes_pii_to_call_fn():
5
+ seen = {}
6
+
7
+ def call_fn(masked_prompt: str) -> str:
8
+ seen["prompt"] = masked_prompt
9
+ return f"Sure, I'll email you back at {masked_prompt.split('at ')[1]}"
10
+
11
+ prompt = "Email me at alice@example.com with the update."
12
+ mask_and_call(prompt, call_fn)
13
+
14
+ assert "alice@example.com" not in seen["prompt"]
15
+ assert "<EMAIL_1>" in seen["prompt"]
16
+
17
+
18
+ def test_mask_and_call_restores_original_values_in_response():
19
+ def call_fn(masked_prompt: str) -> str:
20
+ return f"Got it, confirming: {masked_prompt}"
21
+
22
+ prompt = "My phone number is 415-555-0132."
23
+ result = mask_and_call(prompt, call_fn)
24
+
25
+ assert "415-555-0132" in result
26
+ assert "<PHONE_1>" not in result
27
+
28
+
29
+ def test_mask_and_call_is_provider_agnostic():
30
+ """call_fn is just a plain callable -- swapping providers means swapping the
31
+ closure, not touching mask_and_call itself."""
32
+
33
+ def fake_anthropic_call(masked_prompt: str) -> str:
34
+ return f"[claude] {masked_prompt}"
35
+
36
+ def fake_openai_call(masked_prompt: str) -> str:
37
+ return f"[gpt] {masked_prompt}"
38
+
39
+ prompt = "Contact bob@example.com."
40
+ assert mask_and_call(prompt, fake_anthropic_call) == f"[claude] {prompt}"
41
+ assert mask_and_call(prompt, fake_openai_call) == f"[gpt] {prompt}"
42
+
43
+
44
+ def test_sdk_reexports_core_primitives():
45
+ text = "Reach me at carol@example.com."
46
+ result = mask(text)
47
+
48
+ assert isinstance(result, MaskResult)
49
+ assert unmask(result.masked_text, result.mapping) == text
50
+ assert detect(text)[0].type == PIIType.EMAIL
51
+ assert isinstance(detect(text)[0], Finding)