Skip to main content

fa-redact

PyPI version Python Version License: MIT CI

Languages: English | فارسی


English

fa-redact is a lightweight, zero-dependency, privacy-first Python toolkit for Persian/Iranian Personally Identifiable Information (PII) detection, redaction, and pseudonymization, designed especially for healthcare and AI/LLM applications.

Status: Alpha
Package version in this source tree: v0.2.0.
Published releases are available on PyPI and GitHub Releases. The PyPI badge above reflects the latest published PyPI version.


Table of Contents


Overview

In healthcare, enterprise, and AI workflows, sending raw Persian clinical notes or customer communications to external Large Language Models (LLMs) or third-party APIs poses significant privacy risks. fa-redact addresses these challenges by providing:

  1. Position-Preserving Normalization: Maps Persian and Arabic digits and letters 1-to-1 to canonical representations without altering string length or offset positions.
  2. Algorithmic Validation: Implements checksum rules and prefix checks for Iranian identifiers (National IDs and mobile numbers) rather than relying on loose regular expressions.
  3. Safe Placeholder Redaction: Deterministically substitutes detected spans with typed placeholders.
  4. Stateful Pseudonymization & Local Restoration: Generates stable aliases across conversational turns and restores original values locally inside your trusted boundary.

Why fa-redact?

  • Handling Persian Text Peculiarities: Persian text frequently mixes Persian digits (۰-۹), Arabic-Indic digits (٠-٩), and ASCII digits (0-9), alongside letter variants like Arabic Yeh (ي) and Kaf (ك). Simple regex patterns fail on mixed-script variations, while naive string replacements drift character offsets.
  • Offset Integrity: fa-redact ensures len(normalized) == len(original), guaranteeing that slice offsets text[start:end] map directly back to the original source text.
  • Privacy-First AI Integrations: Mask supported direct identifiers before sending prompts to external LLMs, then restore LLM outputs locally without exposing raw PII outside your trusted perimeter.
  • Zero Runtime Dependencies: Written entirely in pure Python (3.10+) with full typing (py.typed).

Installation

Install the package directly from PyPI:

pip install fa-redact

For development or building from source:

git clone https://github.com/mehdimt1980/fa-redact.git
cd fa-redact
pip install -e ".[dev]"

Quick Start

1. Detect PII

Identify sensitive spans with exact source offsets and normalized values:

from fa_redact import detect

text = "بیمار با کد ملی ۱۲۳۴۵۶۷۸۹۱ و شماره ۰۹۱۲۳۴۵۶۷۸۹ مراجعه کرد."
detections = detect(text)

for d in detections:
    print(
        f"Type: {d.type} | Value: {d.value} | Normalized: {d.normalized_value} | Span: [{d.start}:{d.end}]"
    )
    assert text[d.start : d.end] == d.value

2. Redact PII (Stateless)

Sanitize text by replacing detected spans with deterministic, typed placeholders. Repeated identical identifiers within the same call receive the same placeholder:

from fa_redact import redact

text = "کد ملی: ۱۲۳۴۵۶۷۸۹۱، تماس: ۰۹۱۲۳۴۵۶۷۸۹، تماس مجدد: 09123456789"
redacted_text = redact(text)
print(redacted_text)
# Output: "کد ملی: [IR_NATIONAL_ID_1]، تماس: [IR_MOBILE_1]، تماس مجدد: [IR_MOBILE_1]"

3. Stateful Pseudonymization for AI/LLMs

Maintain consistent entity mappings across conversation turns and restore placeholders locally:

from fa_redact import PseudonymizationSession

session = PseudonymizationSession()

# 1. Pseudonymize prompt locally inside your trusted boundary:
prompt = "کد ملی بیمار ۱۲۳۴۵۶۷۸۹۱ و شماره تماس ۰۹۱۲۳۴۵۶۷۸۹ است."
pseudonymized_prompt = session.pseudonymize(prompt)
print(pseudonymized_prompt)
# Output: "کد ملی بیمار [IR_NATIONAL_ID_1] و شماره تماس [IR_MOBILE_1] است."

# 2. Send ONLY pseudonymized_prompt to external LLM. Simulated LLM response:
llm_response = "جهت هماهنگی با بیمار با [IR_MOBILE_1] تماس حاصل فرمایید."

# 3. Restore placeholders locally:
restored = session.restore(llm_response)
print(restored)
# Output: "جهت هماهنگی با بیمار با ۰۹۱۲۳۴۵۶۷۸۹ تماس حاصل فرمایید."

Detailed Capabilities

1. Position-Preserving Normalization

fa-redact provides pure, deterministic normalization where each character maps 1-to-1 to a normalized code point (len(normalized) == len(original)):

from fa_redact import normalize_digits, normalize_letters, normalize_text

# Normalizes Persian (۰-۹) and Arabic-Indic (٠-٩) digits to ASCII (0-9)
normalize_digits("کد بیمار: ۱۲۳٤٥")
# Returns: "کد بیمار: 12345"

# Normalizes Arabic letter variants (ي -> ی, ك -> ک)
normalize_letters("پزشك و دكتر")
# Returns: "پزشک و دکتر"

# Full position-preserving normalization
normalize_text("كد ملي: ۰۰۱٢٣٤٥٦٧٨")
# Returns: "کد ملی: 0012345678"

What is not normalized: Whitespace, punctuation, zero-width non-joiners (ZWNJ), diacritics, and arbitrary unicode characters are preserved as-is so that source offsets remain perfectly aligned.

2. Detection Model & Pipeline

Identified spans are represented by the immutable Detection dataclass:

  • type: Identifier category (e.g., IR_NATIONAL_ID, IR_MOBILE).
  • value: Exact substring as written in the original input text.
  • normalized_value: Canonical normalized form of the span.
  • start / end: Zero-indexed Python string slice boundaries (text[start:end] == value).

The detection pipeline aggregates detector results and sorts them deterministically by (start, end, type). It intentionally preserves overlaps and duplicate detections. Overlap rejection is applied only when producing replacement text via redact() or PseudonymizationSession.pseudonymize(), where overlapping or nested spans will raise a ValueError.

3. Iranian National ID Validation & Detection

Validates and detects 10-digit Iranian National IDs (کد ملی / Code Melli):

from fa_redact import IranianNationalIDDetector, is_valid_national_id

# Algorithmic test vectors (not personal data)
is_valid_national_id("1234567891")  # True
is_valid_national_id("۱۲۳۴۵۶۷۸۹۱")  # True (Persian digits)
is_valid_national_id("1234567890")  # False (invalid check digit)
is_valid_national_id("1111111111")  # False (repeated digits rejected)
  • Modulo-11 Checksum: Applies the implemented modulo-11 checksum rule for Iranian National IDs.
  • Repeated-Digit Rejection: Rejects pseudo-values like 0000000000 or 1111111111.
  • Exact Length & Formatting: Requires exactly 10 digits in compact format; does not strip spaces, remove hyphens, or auto-pad.
  • Verification Notice: Validation confirms mathematical format only without querying official registries. Checksum validity does not establish whether an identifier has been officially issued to an individual.

4. Iranian Mobile Number Validation & Detection

Validates and detects Iranian mobile numbers using the bundled 2026 CRA/ITU mobile-service NDC allocation snapshot:

from fa_redact import IranianMobileNumberDetector, is_valid_mobile_number

# Supported compact domestic and international formats
is_valid_mobile_number("09123456789")  # True (domestic)
is_valid_mobile_number("۰۹۱۲۳۴۵۶۷۸۹")  # True (Persian digits)
is_valid_mobile_number("+989123456789")  # True (+98 format)
is_valid_mobile_number("00989351234567")  # True (0098 format)
is_valid_mobile_number("09412345678")  # False (fixed non-geographical)
  • Prefix-Aware Allocation: Validates against prefixes allocated for mobile services in the bundled National Numbering Plan snapshot.
  • Exclusion of Non-Mobile Prefixes: Non-mobile prefixes such as 094 (fixed non-geographic) and 09950 (Public Trunk) are rejected.
  • Strict Formatting: Only compact forms (09xxxxxxxxx, +989xxxxxxxxx, 00989xxxxxxxxx) are accepted. The validator does not strip spaces, remove hyphens, or auto-format.
  • Verification Notice: Prefix validation confirms structural numbering-plan compliance only; it does not verify active SIM status, subscriber identity, carrier ownership, or number portability status.

5. Iranian IBAN / Sheba Validation & Detection

fa-redact provides an offline, deterministic Iranian International Bank Account Number (IBAN / شماره شبا) validator (is_valid_iranian_iban) and detector (IranianIBANDetector). An Iranian IBAN in compact electronic form contains 26 characters: IR, followed by 2 check digits and a 22-digit BBAN (or IR followed by 24 numeric digits):

from fa_redact import (
    IranianIBANDetector,
    PseudonymizationSession,
    detect,
    is_valid_iranian_iban,
    redact,
)

# 1. Standalone Validation (Exact 26 chars: uppercase 'IR' + 24 digits, MOD-97)
is_valid_iranian_iban("IR641234567890123456789012")  # True
is_valid_iranian_iban("IR۶۴۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۶۷۸۹۰۱۲")  # True (Persian digits)
is_valid_iranian_iban("ir641234567890123456789012")  # False (lowercase 'ir' rejected)
is_valid_iranian_iban("IR 64 1234 5678 9012 3456 7890 12")  # False (spaces rejected)
is_valid_iranian_iban("IR001234567890123456789012")  # False (invalid checksum)

# 2. Default Detection (Included in default detector set)
text = "شماره شبا واریز: IR641234567890123456789012 و تماس ۰۹۱۲۳۴۵۶۷۸۹"
detections = detect(text)
# Returns: [Detection(type='IR_IBAN', value='IR641234567890123456789012', ...), Detection(type='IR_MOBILE', ...)]

# 3. Default Redaction & Pseudonymization
redacted_text = redact(text)
# Output: "شماره شبا واریز: [IR_IBAN_1] و تماس [IR_MOBILE_1]"

session = PseudonymizationSession()
pseudonymized = session.pseudonymize("واریز به شبا IR641234567890123456789012")
# Output: "واریز به شبا [IR_IBAN_1]"

restored = session.restore("تایید واریز به [IR_IBAN_1]")
# Output: "تایید واریز به IR641234567890123456789012"
  • MOD-97-10 Checksum Algorithm: Validates the standard ISO 7064 MOD 97-10 checksum by rearranging to BBAN (22 digits) + 1827 (IR) + CheckDigits (2 digits) (representing 22-digit BBAN + 1827 + 2 check digits) and verifying remainder == 1.
  • Default Detector Inclusion: Unlike EmailDetector, IranianIBANDetector is included in _DEFAULT_DETECTORS because its structured boundary (IR followed by 24 digits) has no syntactic collision with National IDs or mobile numbers.
  • Position-Preserving & Digit Normalization: Supports Persian (۰-۹) and Arabic-Indic (٠-٩) digits across the 24 numeric characters following IR (the 2 check digits plus the 22-digit BBAN), preserving the raw surface script in Detection.value and mapping to canonical ASCII in Detection.normalized_value.
  • Electronic Compact Format Only: Only compact format without spaces, hyphens, or formatting separators is accepted. Lowercase prefix (ir) is strictly rejected.
  • Privacy & Financial Disclaimer: is_valid_iranian_iban performs purely local, offline mathematical checksum validation. It does not perform bank API lookups, bank branch routing, or account status verification, and does not verify whether an account exists or is active.

6. Conservative ASCII Email Validation & Detection (Opt-in)

fa-redact provides a conservative, zero-dependency ASCII email address validator (is_valid_email) and detector (EmailDetector):

from fa_redact import (
    EmailDetector,
    PseudonymizationSession,
    detect,
    is_valid_email,
    redact,
)

# 1. Standalone Validation
is_valid_email("user.name@example.com")  # True
is_valid_email("doctor@sub.hospital.ir")  # True
is_valid_email("user@localhost")  # False (single-label domain rejected)
is_valid_email("user+tag@invalid_domain.com")  # False (underscore in domain)
is_valid_email("user@مثال.ایران")  # False (non-ASCII / EAI unsupported)

# 2. Opt-in Detection (Pass EmailDetector explicitly)
text = "مکاتبه با دکتر احمدی: dr.ahmadi@hospital.ir و تماس 09123456789"
detections = detect(text, detectors=[EmailDetector()])
# Returns: [Detection(type='EMAIL', value='dr.ahmadi@hospital.ir', ...)]

# 3. Opt-in Redaction & Pseudonymization
redacted_text = redact(text, detectors=[EmailDetector()])
# Output: "مکاتبه با دکتر احمدی: [EMAIL_1] و تماس 09123456789"

session = PseudonymizationSession()
pseudonymized = session.pseudonymize(
    "تماس با info@clinic.ir یا dr.ahmadi@hospital.ir انجام شد.",
    detectors=[EmailDetector()],
)
# Output: "تماس با [EMAIL_1] یا [EMAIL_2] انجام شد."

restored = session.restore("پیام به [EMAIL_1] ارسال شد.")
# Output: "پیام به info@clinic.ir ارسال شد."
  • Opt-in Architecture: EmailDetector is intentionally opt-in in Phase 12 and is not included in the default detector set. Numeric-looking email local parts (such as 09123456789@example.com or 1234567891@example.com) can produce overlapping spans with mobile number or National ID detectors; while detect() permits overlaps, downstream redaction and pseudonymization fail-loud on overlaps until general conflict resolution is implemented.
  • Per-Call Detector Configuration: PseudonymizationSession stores pseudonym mappings and counters, not a persistent detector configuration. The detector set is selected independently for each pseudonymize() call.
  • Conservative ASCII Specification: Validates dot-atom local parts (1-64 chars) and DNS-style domain names (1-253 total domain chars, 1-63 chars per label, 2-63 chars for TLD, total address <= 254 characters).
  • Unsupported Complex/Obsolete Forms: Quoted local parts ("john doe"@example.com), IP domain literals (user@[192.168.1.1]), comments, folding whitespace, single-label domains (user@localhost), and internationalized/Unicode email addresses (EAI / RFC 6530+) are rejected.
  • Privacy & Verification Disclaimer: is_valid_email performs purely local, offline syntactic validation. It performs no DNS queries, MX record lookups, mailbox verification, or network requests, and logs no PII. Syntactic validity does not verify that a mailbox exists or is deliverable.

7. Bank Card / PAN Validation & Detection (Opt-in)

fa-redact provides an offline, deterministic 16-digit payment card (Primary Account Number / PAN) validator (is_valid_bank_card_number) and detector (BankCardDetector):

from fa_redact import (
    BankCardDetector,
    PseudonymizationSession,
    detect,
    is_valid_bank_card_number,
    redact,
)

# 1. Standalone Validation (Exact 16 digits, Luhn MOD-10 checksum)
is_valid_bank_card_number("1234567890123452")  # True (valid synthetic card)
is_valid_bank_card_number("۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۲")  # True (Persian digits)
is_valid_bank_card_number("١٢٣٤٥٦٧٨٩٠١٢٣٤٥٢")  # True (Arabic-Indic digits)
is_valid_bank_card_number("1234 5678 9012 3452")  # False (spaces rejected)
is_valid_bank_card_number("1234-5678-9012-3452")  # False (hyphens rejected)
is_valid_bank_card_number("0000000000000000")  # False (all-identical rejected)
is_valid_bank_card_number("1234567890123453")  # False (checksum mismatch)

# 2. Opt-in Detection (Pass BankCardDetector explicitly)
text = "شماره کارت: 1234567890123452"
detections = detect(text, detectors=[BankCardDetector()])
# Returns: [Detection(type='BANK_CARD', value='1234567890123452', ...)]

# 3. Opt-in Redaction & Pseudonymization
redacted_text = redact(text, detectors=[BankCardDetector()])
# Output: "شماره کارت: [BANK_CARD_1]"

session = PseudonymizationSession()
pseudonymized = session.pseudonymize(
    "واریز به کارت ۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۲ انجام شد.",
    detectors=[BankCardDetector()],
)
# Output: "واریز به کارت [BANK_CARD_1] انجام شد."

restored = session.restore("تایید واریز به [BANK_CARD_1]")
# Output: "تایید واریز به ۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۲"
  • Opt-in Architecture: BankCardDetector is intentionally opt-in and is not included in the default detector set (_DEFAULT_DETECTORS).
  • Standard Luhn MOD-10 Checksum: Validates 16-digit payment card numbers using the standard Luhn algorithm (doubling digits at odd offsets from the right, subtracting 9 if the product exceeds 9, and verifying sum % 10 == 0).
  • Position-Preserving Digit Normalization: Accepts ASCII (0-9), Persian (۰-۹), and Arabic-Indic (٠-٩) digits. Preserves the exact surface script in Detection.value and normalizes to canonical ASCII in Detection.normalized_value.
  • Defensive Sequence Filtering: Trivial all-identical sequences (such as 0000000000000000 through 9999999999999999) are defensively rejected regardless of their Luhn checksum status.
  • Strict Electronic Compact Format: Only compact 16-digit strings without spaces, hyphens, or formatting separators are accepted. The validator does not strip whitespace, remove hyphens, or clean up formatting.
  • Issuer Neutrality: BankCardDetector uses issuer-neutral terminology and entities (BANK_CARD). The library does not maintain an Iranian BIN/IIN registry and does not verify whether a card was issued by an Iranian bank or any specific card network.
  • Privacy & Financial Disclaimer: is_valid_bank_card_number performs purely local, offline mathematical checksum validation. It does not perform payment gateway verification, card activation status checks, CVV2/expiry checks, or account balance lookups. Checksum validity does not prove that a payment card exists, is active, or belongs to a specific cardholder.

8. Configurable Institutional / Healthcare Identifiers (Opt-in)

MRNs (Medical Record Numbers), Patient IDs, Admission IDs, Encounter IDs, Case IDs, and similar clinical/enterprise identifiers are institution-specific. fa-redact does not pretend there is one universal regex for them, nor does it hardcode synthetic assumptions into package defaults.

Instead, fa-redact provides a lightweight, immutable configuration layer (PatternRule and PatternDetector) allowing hospitals and applications to supply their own identifier patterns:

import re
from fa_redact import (
    PatternDetector,
    PatternRule,
    PseudonymizationSession,
    detect,
    redact,
)

# 1. Define institution-specific identifier rules
hospital_detector = PatternDetector(
    [
        # Standard full-match identifier (MRN)
        PatternRule(
            type="MRN",
            pattern=r"(?<!\w)MRN-[0-9]{6}(?!\w)",
        ),
        # Context-aware rule extracting only the ID using a capture group
        PatternRule(
            type="PATIENT_ID",
            pattern=r"Patient\s*ID\s*:\s*(?P<id>PAT-[A-Z]{2}-[0-9]{8})",
            group="id",
            flags=re.IGNORECASE,
        ),
        # Admission ID
        PatternRule(
            type="ADMISSION_ID",
            pattern=r"(?<!\w)ADM-20[0-9]{2}-[0-9]{6}(?!\w)",
        ),
        # Encounter ID
        PatternRule(
            type="ENCOUNTER_ID",
            pattern=r"(?<!\w)ENC-[0-9]{10}(?!\w)",
        ),
    ]
)

# 2. Detect with normalized Persian digits matching ASCII regexes
text = "پرونده: MRN-۱۲۳۴۵۶ و Patient ID: PAT-TE-12345678"
detections = detect(text, detectors=[hospital_detector])
# Returns:
# - Detection(type='MRN', value='MRN-۱۲۳۴۵۶', normalized_value='MRN-123456', span=[8:18])
# - Detection(type='PATIENT_ID', value='PAT-TE-12345678', normalized_value='PAT-TE-12345678', span=[33:48])

# 3. Redact (leaving context labels like 'Patient ID: ' untouched)
redacted = redact(text, detectors=[hospital_detector])
# Output: "پرونده: [MRN_1] و Patient ID: [PATIENT_ID_1]"

# 4. Stateful Pseudonymization across mixed scripts
session = PseudonymizationSession()
# Turn 1: Persian digits
turn1 = session.pseudonymize("پرونده: MRN-۱۲۳۴۵۶", detectors=[hospital_detector])
# Output: "پرونده: [MRN_1]"

# Turn 2: ASCII digits (resolves to same [MRN_1] identity)
turn2 = session.pseudonymize("پیگیری پرونده MRN-123456", detectors=[hospital_detector])
# Output: "پیگیری پرونده [MRN_1]"

# Restore recovers first-observed Persian representation locally
restored = session.restore("پاسخ به [MRN_1]")
# Output: "پاسخ به MRN-۱۲۳۴۵۶"
  • Opt-in Architecture: PatternDetector is strictly opt-in and is not included in _DEFAULT_DETECTORS. Explicit detectors=[hospital_detector] replaces default detectors for that call.
  • Position-Preserving Digit Normalization: By default (source="normalized"), patterns match against position-preserving normalized text. A single ASCII regex such as MRN-[0-9]{6} matches Persian (MRN-۱۲۳۴۵۶), Arabic-Indic (MRN-١٢٣٤٥٦), and ASCII (MRN-123456) forms, preserving surface script in Detection.value and mapping to identical (type, normalized_value) pseudonym identities.
  • Raw Matching Mode: For institutions requiring candidate matching strictly on unnormalized text, source="original" is supported.
  • Context-Aware Capture Groups: Supports integer indices (group=1) and named capture groups (group="id"), allowing the detector to match context prefixes (e.g. Patient ID: ) while extracting only the identifier into the detection span. Nonparticipating capture groups fail loud with a ValueError.
  • Regex Boundary Ownership: PatternDetector does not wrap configured patterns with word boundaries (\b or (?<!\w)). Institutions own full regex semantics.
  • Pre-Compiled & Immutable: PatternRule is an immutable frozen dataclass. Rules are compiled once during PatternDetector construction.

9. Explicit Detection Conflict Resolution (Opt-in)

detect() always returns raw detector evidence and intentionally preserves overlapping and duplicate detections. This design allows callers to audit and inspect raw matches without hidden priority heuristics.

By default, downstream functions (redact() and PseudonymizationSession.pseudonymize()) operate under conflict_policy="reject", failing loudly if any overlapping, nested, or duplicate detections exist:

from fa_redact import BankCardDetector, EmailDetector, redact

text = "ایمیل: 1234567890123452@example.com"
detectors = [EmailDetector(), BankCardDetector()]

# Default reject policy: raises ValueError because EMAIL and BANK_CARD spans overlap
# redact(text, detectors=detectors)  # -> ValueError: Overlapping detections at spans [7:35] (EMAIL) and [7:23] (BANK_CARD)

For callers who understand the ambiguity and choose to resolve conflicts explicitly, fa-redact provides two opt-in resolution policies:

1. Longest Span Policy (conflict_policy="longest")

Greedily selects the longest matching span and discards shorter overlapping candidates. Exact duplicates are collapsed into one. Ambiguous equal-length overlapping spans raise a ValueError:

# Longest policy selects the full EMAIL span (length 28) over BANK_CARD (length 16)
redacted_longest = redact(text, detectors=detectors, conflict_policy="longest")
# Output: "ایمیل: [EMAIL_1]"
2. Explicit Type Priority Policy (conflict_policy="priority")

Resolves conflicts based on an explicit user-configured sequence of entity types in descending priority order (type_priority=[...]). Higher priority types win over lower priority types regardless of span length:

# Priority: BANK_CARD > EMAIL
card_first = redact(
    text,
    detectors=detectors,
    conflict_policy="priority",
    type_priority=["BANK_CARD", "EMAIL"],
)
# Output: "ایمیل: [BANK_CARD_1]@example.com"

# Priority: EMAIL > BANK_CARD
email_first = redact(
    text,
    detectors=detectors,
    conflict_policy="priority",
    type_priority=["EMAIL", "BANK_CARD"],
)
# Output: "ایمیل: [EMAIL_1]"
Standalone Conflict Resolver for Auditing

Callers can inspect and resolve raw detections independently before redaction:

from fa_redact import detect, resolve_detection_conflicts

raw_detections = detect(text, detectors=detectors)
# Returns 2 overlapping detections: EMAIL and BANK_CARD

resolved_detections = resolve_detection_conflicts(
    raw_detections,
    policy="longest",
)
# Returns 1 detection: EMAIL

10. Redaction Semantics

  • Exact Span Reconstruction: redact() rebuilds the output from the original Detection spans, preserving untouched source slices exactly and replacing only detected spans. It does not perform global value-based str.replace().
  • Typed Placeholders: Placeholders follow the format [<TYPE>_<INDEX>] (e.g., [IR_NATIONAL_ID_1], [IR_MOBILE_1], [IR_IBAN_1], [EMAIL_1], [BANK_CARD_1], [MRN_1]).
  • Deterministic Numbering: Identifiers receive sequential numbering based on their order of first appearance.
  • Collision Avoidance: If an input already contains a literal string matching the placeholder syntax, newly generated placeholders increment past the colliding index.
  • Fail-Loud on Overlap by Default: Under default conflict_policy="reject", if overlapping or duplicate spans are passed to redact(), it raises a ValueError.

11. Stateful Pseudonymization Sessions

PseudonymizationSession manages persistent mappings across multi-turn AI interactions:

  • Local Sensitive Mapping: session.mapping contains {placeholder: original_raw_value}. Keep this mapping strictly inside your trusted boundary.
  • Cross-Call Identity: Entities are tracked by (type, normalized_value). For example, ۰۹۱۲۳۴۵۶۷۸۹ in turn 1 and 09123456789 in turn 2 both resolve to [IR_MOBILE_1].
  • Domestic vs. International Limitation: 09123456789 and +989123456789 have different normalized strings and are not canonicalized into the same identity in v0.2.0.
  • First-Observed Representative Restoration: Placeholders are restored using the first-observed raw representation. Restoration is semantic placeholder restoration and is not guaranteed to reconstruct the exact original surface representation of every occurrence byte-for-byte.
  • Non-Cascading Restoration: restore() performs an escaped single-pass replacement, preventing recursive expansion if restored values contain placeholder syntax.
  • Atomic Updates: If a call fails during processing, the session state, mappings, and counters remain unmodified.
  • Historical Literal Token Reservation: Literal placeholder-like tokens observed in prior calls remain reserved so they are never assigned to real PII in later calls.
  • Unknown Placeholders: Unmapped placeholders (e.g., [IR_MOBILE_99]) are left untouched without error.

Custom Detectors

fa-redact uses Python's structural typing (protocols). Any class implementing the two-argument detect(self, original_text: str, normalized_text: str) -> Sequence[Detection] method can be passed to detect(), redact(), or session.pseudonymize():

import re
from collections.abc import Sequence

from fa_redact import Detection, detect


class MedicalRecordNumberDetector:
    """Example custom detector for synthetic institutional MRNs."""

    def detect(
        self,
        original_text: str,
        normalized_text: str,
    ) -> Sequence[Detection]:
        detections: list[Detection] = []

        for match in re.finditer(r"\bMRN-\d{6}\b", normalized_text):
            detections.append(
                Detection.from_texts(
                    type="MRN",
                    original_text=original_text,
                    normalized_text=normalized_text,
                    start=match.start(),
                    end=match.end(),
                )
            )

        return detections


# Note: Passing an explicit detector list replaces the default detector set for that call.
text = "پرونده با MRN-123456 ثبت شد."
detections = detect(text, detectors=[MedicalRecordNumberDetector()])

Healthcare & AI/LLM Usage Pattern

Local Hospital / Trusted Boundary
  │
  ├── 1. Raw clinical text with sensitive identifiers
  │      ↓
  ├── 2. session.pseudonymize(raw_text)
  │      ↓
  ├── 3. Pseudonymized prompt (supported identifiers replaced)
  │
  ▼  (Transmit ONLY sanitized text to External AI / Cloud)
External LLM API (e.g., Clinical Summary / Diagnostics)
  ▲
  │  (Receive response containing placeholders)
  ▼
Local Hospital / Trusted Boundary
  │
  └── 4. session.restore(llm_response)
         ↓
      5. Final clinical report with restored original identifiers

Current Coverage & Limitations

Identifier / Capability v0.1.0 v0.2.0 Notes
Iranian National ID (کد ملی) ✅ Supported ✅ Default Strict 10-digit modulo-11 checksum validation
Iranian Mobile Number ✅ Supported ✅ Default Prefix-aware validation against 2026 CRA numbering plan
Iranian IBAN / Sheba (شبا) ❌ Not Supported ✅ Default Strict 26-char MOD-97 checksum validation (IR + 24 digits)
Email Addresses ❌ Not Supported 🧪 Opt-in Conservative ASCII email validation and detection (detectors=[EmailDetector()])
16-digit Bank Card (PAN) ❌ Not Supported 🧪 Opt-in 16-digit compact PAN + Luhn checksum validation (detectors=[BankCardDetector()])
Institutional / Healthcare IDs (MRN, Patient ID) ❌ Not Supported 🧪 Opt-in Configurable via user-defined PatternRule / PatternDetector
Explicit Conflict Resolution ❌ Not Supported 🧪 Opt-in Policy Resolves overlaps/duplicates via "longest" or "priority" policy
Personal Names ❌ Not Supported ❌ Not Supported Planned for future versions (requires NER/contextual models)
Postal Addresses ❌ Not Supported ❌ Not Supported Unstructured spatial entities
Dates of Birth / Timestamps ❌ Not Supported ❌ Not Supported Planned for future versions
Health Insurance Numbers ❌ Not Supported ❌ Not Supported Institution-specific

Privacy and Security Model


Privacy-Safe Test Data Policy

All examples, test fixtures, and documentation in fa-redact are constructed from synthetic test vectors, algorithmic patterns, and non-personal sample data. No real patient records, clinical charts, credentials, or personal datasets are used or included in the repository.


Development & Quality Checks

Run the test suite:

python -m pytest

Check code formatting and linting:

ruff check .
ruff format --check .

Run static type checking:

mypy src
mypy tests

Build and validate distribution packages:

python -m build
python -m twine check dist/*

For guidelines on contributing, see CONTRIBUTING.md and SECURITY.md.


Release Process

For release procedures and PyPI publishing setup using GitHub Actions OIDC Trusted Publishing, see RELEASING.md.


License

This project is licensed under the MIT License.


فارسی

fa-redact یک کتابخانهٔ پایتونی سبک، مستقل (بدون وابستگی خارجی / Zero-dependency) و مبتنی بر حریم خصوصی (Privacy-first) است که با هدف تشخیص (Detection)، پنهان‌سازی (Redaction) و نام‌مستعارسازی (Pseudonymization) اطلاعات هویتی و حساس در متون فارسی و داده‌های مرتبط با ایران طراحی شده است.

وضعیت: آلفا (Alpha)
نسخه بسته در این درخت منبع: v0.2.0
نسخه‌های منتشرشده در PyPI و گیت‌هاب در دسترس هستند. نشان (Badge) بالای صفحه آخرین نسخهٔ منتشرشده در PyPI را نمایش می‌دهد.


فهرست مطالب


نمای کلی (Overview)

در سیستم‌های درمانی، سازمانی و کاربردهای نوین هوش مصنوعی، ارسال متن‌های حاوی اطلاعات شناسایی‌کنندهٔ شخصی (Personally Identifiable Information یا به اختصار PII) به مدل‌های زبانی بزرگ (LLM) یا سرویس‌های ابری خارجی می‌تواند حریم خصوصی بیماران و کاربران را به خطر بیندازد.

کتابخانهٔ fa-redact با ارائهٔ ابزارهای سبک و کارآمد، متن را پیش از خروج از محیط امن شما پردازش کرده و شناسه‌های حساس پشتیبانی‌شده را با نشان‌گذارهای جایگزین (Placeholders) تعویض می‌کند. پس از دریافت پاسخ از هوش مصنوعی، می‌توان نشان‌گذارها را در محیط محلی و امن خود به مقادیر اصلی بازگرداند (Restoration).


چرا fa-redact؟ (Motivation)

پردازش متون فارسی در زمینهٔ امنیت داده و پالایش متون، چالش‌های منحصربه‌فردی دارد:

  1. تنوع ارقام و حروف: در متون فارسی، شماره‌ها ممکن است با ارقام فارسی (۰-۹)، ارقام عربی (٠-٩) یا ارقام لاتین (0-9) نوشته شده باشند. همچنین تفاوت حروف مانند «ي» و «ك» عربی با «ی» و «ک» فارسی مانع عملکرد صحیح الگوهای ساده می‌شود.
  2. حفظ موقعیت کاراکترها (Offsets): روش‌های مرسوم نرم‌سازی متن اغلب طول رشته را تغییر می‌دهند (مثلاً با حذف فاصله‌ها یا اعراب)؛ این کار باعث جابه‌جایی ایندکس‌های کاراکتری شده و جایگزینی دقیق در متن اصلی را غیرممکن می‌سازد. fa-redact تضمین می‌کند که طول رشته قبل و بعد از نرم‌سازی یکسان باشد (len(normalized) == len(original)).
  3. اعتبارسنجی الگوریتمی: به‌جای استفاده از رجکس‌های حدسی، شناسه‌های ملی و شماره‌های موبایل با قواعد کنترلی و پیش‌شماره‌های رگولاتوری ایران بررسی می‌شوند.
  4. حفظ یکپارچگی ارجاعات در هوش مصنوعی: در مکالمات چندمرحله‌ای با LLMها، یک موجودیت مشخص همواره به یک نام‌مستعار یکسان نگاشت می‌شود تا ساختار معنایی متن برای مدل حفظ شود.
  5. بدون وابستگی و کاملاً تایپ‌شده: بدون نیاز به بسته‌های جانبی سنگین، کاملاً با پایتون خالص (+3.10) پیاده‌سازی شده و از تایپینگ کامل (py.typed) پشتیبانی می‌کند.

نصب (Installation)

نصب آخرین نسخهٔ پایدار از طریق مخزن رسمی PyPI:

pip install fa-redact

نصب برای توسعه‌دهندگان و از روی کد منبع:

git clone https://github.com/mehdimt1980/fa-redact.git
cd fa-redact
pip install -e ".[dev]"

شروع سریع (Quick Start)

۱. تشخیص اطلاعات هویتی (detect)

شناسایی بخش‌های حساس متن به همراه موقعیت دقیق کاراکتری و مقدار نرمال‌شده:

from fa_redact import detect

text = "بیمار با کد ملی ۱۲۳۴۵۶۷۸۹۱ و شماره ۰۹۱۲۳۴۵۶۷۸۹ مراجعه کرد."
detections = detect(text)

for item in detections:
    print(f"نوع شناسه: {item.type}")
    print(f"مقدار در متن اصلی: {item.value}")
    print(f"مقدار نرمال‌شده: {item.normalized_value}")
    print(f"موقعیت کاراکتری: [{item.start}:{item.end}]")

    # اطمینان از تطابق دقیق موقعیت با متن اصلی:
    assert text[item.start : item.end] == item.value

فیلدهای شیء Detection:

  • type: نوع شناسهٔ شناسایی‌شده (مانند IR_NATIONAL_ID یا IR_MOBILE).
  • value: مقدار دقیق همان‌طور که در متن ورودی نوشته شده است.
  • normalized_value: مقدار نرمال‌شده و یکپارچهٔ شناسه.
  • start و end: موقعیت شروع و پایان شناسه در متن ورودی (text[start:end] == value).

۲. پنهان‌سازی بدون‌حالت (redact)

جایگزینی شناسه‌ها با نشان‌گذارهای نوع‌دار و امن. در هر فراخوانی، شناسه‌های تکراری نشان‌گذار یکسانی دریافت می‌کنند:

from fa_redact import redact

text = "کد ملی: ۱۲۳۴۵۶۷۸۹۱، تماس: ۰۹۱۲۳۴۵۶۷۸۹، تماس مجدد: 09123456789"
redacted_text = redact(text)
print(redacted_text)
# خروجی: "کد ملی: [IR_NATIONAL_ID_1]، تماس: [IR_MOBILE_1]، تماس مجدد: [IR_MOBILE_1]"
  • تابع redact() بدون‌حالت (Stateless) است و نگاشتی برای بازگردانی ذخیره نمی‌کند.
  • شماره‌گذاری در هر فراخوانی از 1 آغاز می‌شود.

۳. نام‌مستعارسازی و بازگردانی برای هوش مصنوعی (PseudonymizationSession)

مدیریت حالت در تعاملات چندمرحله‌ای با مدل‌های زبانی (LLM) و بازگردانی محلی پاسخ‌ها:

from fa_redact import PseudonymizationSession

# ایجاد یک نشست جدید
session = PseudonymizationSession()

# ۱. پنهان‌سازی متن حساس پیش از ارسال به سرویس خارجی:
prompt = "کد ملی بیمار ۱۲۳۴۵۶۷۸۹۱ و شماره تماس ۰۹۱۲۳۴۵۶۷۸۹ است."
pseudonymized_prompt = session.pseudonymize(prompt)
print(pseudonymized_prompt)
# خروجی: "کد ملی بیمار [IR_NATIONAL_ID_1] و شماره تماس [IR_MOBILE_1] است."

# ۲. ارسال فقط متن نام‌مستعارسازی‌شده (pseudonymized_prompt) به مدل زبانی. پاسخ فرضی مدل:
llm_response = "جهت هماهنگی با بیمار با [IR_MOBILE_1] تماس حاصل فرمایید."

# ۳. بازگردانی مقادیر واقعی به صورت محلی در محیط امن شما:
restored = session.restore(llm_response)
print(restored)
# خروجی: "جهت هماهنگی با بیمار با ۰۹۱۲۳۴۵۶۷۸۹ تماس حاصل فرمایید."

قابلیت‌های تفصیلی

۱. نرم‌سازی با حفظ موقعیت کاراکترها (Position-Preserving Normalization)

در fa-redact نرم‌سازی به شیوه‌ای کاملاً قطعی و نگاشت یک‌به‌یک کاراکترها انجام می‌شود تا ایندکس کاراکترها هیچ تغییری نکند (len(normalized) == len(original)):

from fa_redact import normalize_digits, normalize_letters, normalize_text

# تبدیل ارقام فارسی (۰-۹) و عربی (٠-٩) به ارقام اسکی (0-9)
normalize_digits("کد بیمار: ۱۲۳٤٥")
# خروجی: "کد بیمار: 12345"

# تبدیل حروف عربی به فارسی (ي -> ی ، ك -> ک)
normalize_letters("پزشك و دكتر")
# خروجی: "پزشک و دکتر"

# نرم‌سازی جامع با حفظ طول رشته
normalize_text("كد ملي: ۰۰۱٢٣٤٥٦٧٨")
# خروجی: "کد ملی: 0012345678"

این توابع فاصله‌ها، نیم‌فاصله‌ها، اعراب یا علائم نگارشی را حذف نمی‌کنند تا آفست‌ها با دقت صد در صد حفظ شوند.

۲. مدل داده و پایپ‌لاین تشخیص

تابع detect() نتایج تشخیص‌دهنده‌ها را جمع‌آوری و بر اساس (start, end, type) مرتب می‌کند. این لایه عمداً همپوشانی یا تشخیص‌های تکراری را حذف یا رد نمی‌کند.

کنترل همپوشانی زمانی اعمال می‌شود که قرار است متن واقعاً تغییر کند؛ یعنی در redact() و PseudonymizationSession.pseudonymize(). در این دو API، وجود spanهای همپوشان، تودرتو یا تکراری باعث ValueError می‌شود تا از جایگزینی‌های اشتباه و مخدوش شدن داده‌های محرمانه جلوگیری شود.

۳. اعتبارسنجی و تشخیص کد ملی ایران

تشخیص و اعتبارسنجی کد ملی ۱۰ رقمی با قاعدهٔ کنترل Modulo-11 پیاده‌سازی‌شده برای کد ملی ایران:

from fa_redact import IranianNationalIDDetector, is_valid_national_id

# مقادیر نمونهٔ الگوریتمی (غیرواقعی):
is_valid_national_id("1234567891")  # True (معتبر)
is_valid_national_id("۱۲۳۴۵۶۷۸۹۱")  # True (پشتیبانی از ارقام فارسی)
is_valid_national_id("1234567890")  # False (رقم کنترلی نامعتبر)
is_valid_national_id("1111111111")  # False (رد ارقام تکراری جعلی)
  • بررسی چکسام: اعمال قاعدهٔ کنترلی Modulo-11 کد ملی ایران.
  • رد کدهای تکراری جعلی: کدهایی مانند 0000000000 یا 1111111111 که فرمول چکسام را پاس می‌کنند اما نامعتبرند، رد می‌شوند.
  • طول و قالب دقیق: ورودی باید دقیقاً ۱۰ رقم در قالب فشرده باشد؛ فاصله‌ها یا خط تیره حذف نمی‌شوند و صفرهای ابتدایی به‌طور خودکار اضافه نمی‌شوند.
  • سلب مسئولیت استعلام: این اعتبارسنجی صرفاً ساختار ریاضی را تایید می‌کند و استعلامی از سامانه‌های ثبت احوال یا احراز هویت انجام نمی‌دهد.

۴. اعتبارسنجی و تشخیص شماره موبایل ایران

اعتبارسنجی بر اساس snapshot شماره‌گذاری ۲۰۲۶ CRA/ITU و پیش‌شماره‌هایی انجام می‌شود که در آن برای خدمات موبایل تخصیص یافته‌اند:

from fa_redact import IranianMobileNumberDetector, is_valid_mobile_number

# قالب‌های فشردهٔ داخلی و بین‌المللی پشتیبانی‌شده
is_valid_mobile_number("09123456789")  # True (قالب داخلی)
is_valid_mobile_number("۰۹۱۲۳۴۵۶۷۸۹")  # True (ارقام فارسی)
is_valid_mobile_number("+989123456789")  # True (قالب بین‌المللی با +98)
is_valid_mobile_number("00989351234567")  # True (قالب بین‌المللی با 0098)
is_valid_mobile_number("09412345678")  # False (شماره ثابت غیرجغرافیایی)
  • تفکیک پیش‌شماره‌های خدمات سیار: بررسی پیش‌شماره‌ها بر اساس snapshot طرح شماره‌گذاری کشوری.
  • رد پیش‌شماره‌های غیرموبایل: پیش‌شماره‌هایی نظیر 094 (ثابت غیرجغرافیایی) یا 09950 (ترانک عمومی) به عنوان موبایل پذیرفته نمی‌شوند.
  • قالب فشرده: فقط قالب‌های فشرده (09xxxxxxxxx، +989xxxxxxxxx، 00989xxxxxxxxx) پذیرفته می‌شوند و حذف فاصله، پرانتز یا خط تیره انجام نمی‌شود.
  • سلب مسئولیت مالکیت: اعتبارسنجی ساختاری است و وضعیت فعال بودن سیم‌کارت، هویت مشترک، اپراتور فعلی یا ترابردپذیری را بررسی نمی‌کند.

۵. اعتبارسنجی و تشخیص شماره شبا / IBAN ایران

کتابخانهٔ fa-redact تابع اعتبارسنجی مستقل is_valid_iranian_iban و تشخیص‌دهندهٔ IranianIBANDetector را برای شماره شبا / شناسه حساب بانکی ایران (IBAN) به صورت کاملاً آفلاین، قطعی و بدون وابستگی خارجی ارائه می‌دهد. ساختار شماره شبا در قالب فشرده الکترونیکی شامل ۲۶ کاراکتر است: پیشوند IR، به همراه ۲ رقم کنترلی و BBAN بیست‌ودورقمی (یا IR به همراه ۲۴ رقم عددی):

from fa_redact import (
    IranianIBANDetector,
    PseudonymizationSession,
    detect,
    is_valid_iranian_iban,
    redact,
)

# ۱. اعتبارسنجی مستقل (دقیقاً ۲۶ کاراکتر: IR بزرگ + ۲۴ رقم با الگوریتم MOD-97):
is_valid_iranian_iban("IR641234567890123456789012")  # True (معتبر)
is_valid_iranian_iban("IR۶۴۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۶۷۸۹۰۱۲")  # True (پشتیبانی از ارقام فارسی)
is_valid_iranian_iban("ir641234567890123456789012")  # False (رد حروف کوچک ir)
is_valid_iranian_iban("IR 64 1234 5678 9012 3456 7890 12")  # False (رد فاصله)
is_valid_iranian_iban("IR001234567890123456789012")  # False (چکسام نامعتبر)

# ۲. تشخیص پیش‌فرض (قرارگیری در مجموعهٔ تشخیص‌دهنده‌های پیش‌فرض):
text = "شماره شبا واریز: IR641234567890123456789012 و تماس ۰۹۱۲۳۴۵۶۷۸۹"
detections = detect(text)
# خروجی: [Detection(type='IR_IBAN', ...), Detection(type='IR_MOBILE', ...)]

# ۳. پنهان‌سازی و نام‌مستعارسازی پیش‌فرض:
redacted_text = redact(text)
# خروجی: "شماره شبا واریز: [IR_IBAN_1] و تماس [IR_MOBILE_1]"

session = PseudonymizationSession()
pseudonymized = session.pseudonymize("واریز به شبا IR641234567890123456789012")
# خروجی: "واریز به شبا [IR_IBAN_1]"

restored = session.restore("تایید واریز به [IR_IBAN_1]")
# خروجی: "تایید واریز به IR641234567890123456789012"
  • الگوریتم چکسام MOD-97-10: اعتبارسنجی استاندارد بین‌المللی ISO 7064 MOD 97-10 از طریق بازآرایی رشته به فرمت BBAN (۲۲ رقم) + 1827 (IR) + ارقام کنترلی (۲ رقم) و بررسی شرط باقیمانده == 1.
  • حضور در پایپ‌لاین پیش‌فرض: برخلاف EmailDetector، تشخیص‌دهندهٔ IranianIBANDetector در مجموعهٔ پیش‌فرض (_DEFAULT_DETECTORS) قرار دارد؛ زیرا ساختار مشخص آن (IR به همراه ۲۴ رقم) هیچ‌گونه تداخل یا ابهام سینتکسی با کدهای ملی یا شماره‌های موبایل ایجاد نمی‌کند.
  • حفظ موقعیت کاراکتری و پشتیبانی از ارقام متنوع: امکان استفاده از ارقام فارسی (۰-۹) و عربی (٠-٩) در ۲۴ نویسهٔ عددی پس از IR وجود دارد؛ این بخش شامل ۲ رقم کنترلی و BBAN بیست‌ودورقمی است. نگاشت در Detection.value بر اساس متن ورودی حفظ شده و مقدار یکپارچه در Detection.normalized_value قرار می‌گیرد.
  • قالب فشردهٔ الکترونیکی: صرفاً فرمت الکترونیکی فشرده بدون فاصله، خط تیره یا جداکننده پذیرفته می‌شود. پیشوند ir با حروف کوچک نیز پذیرفته نمی‌شود.
  • سلب مسئولیت بانکی و حریم خصوصی: تابع is_valid_iranian_iban صرفاً صحت ساختار ریاضی چکسام را به صورت محلی و آفلاین بررسی می‌کند. این تابع هیچ‌گونه استعلام بانکی، بررسی صحت شماره حساب، اتصال به شبکهٔ شتاب یا پایا انجام نمی‌دهد و فعال بودن حساب بانکی را تایید نمی‌کند.

۶. اعتبارسنجی و تشخیص آدرس ایمیل اسکی (اختیاری)

کتابخانهٔ fa-redact تابع اعتبارسنجی مستقل is_valid_email و تشخیص‌دهندهٔ EmailDetector را برای آدرس‌های ایمیل استاندارد اسکی به صورت بدون وابستگی ارائه می‌دهد:

from fa_redact import (
    EmailDetector,
    PseudonymizationSession,
    detect,
    is_valid_email,
    redact,
)

# ۱. اعتبارسنجی ساختاری مستقل:
is_valid_email("user.name@example.com")  # True (معتبر)
is_valid_email("doctor@sub.hospital.ir")  # True (معتبر)
is_valid_email("user@localhost")  # False (رد دامنه‌های تک‌بخشی)
is_valid_email("user+tag@invalid_domain.com")  # False (رد آندرلاین در دامنه)
is_valid_email("user@مثال.ایران")  # False (عدم پشتیبانی از ایمیل‌های غیر اسکی)

# ۲. تشخیص با فعال‌سازی اختیاری (Opt-in):
text = "مکاتبه با دکتر احمدی: dr.ahmadi@hospital.ir و تماس 09123456789"
detections = detect(text, detectors=[EmailDetector()])
# خروجی: [Detection(type='EMAIL', value='dr.ahmadi@hospital.ir', ...)]

# ۳. پنهان‌سازی و نام‌مستعارسازی اختیاری:
redacted_text = redact(text, detectors=[EmailDetector()])
# خروجی: "مکاتبه با دکتر احمدی: [EMAIL_1] و تماس 09123456789"

session = PseudonymizationSession()
pseudonymized = session.pseudonymize(
    "تماس با info@clinic.ir یا dr.ahmadi@hospital.ir انجام شد.",
    detectors=[EmailDetector()],
)
# خروجی: "تماس با [EMAIL_1] یا [EMAIL_2] انجام شد."

restored = session.restore("پیام به [EMAIL_1] ارسال شد.")
# خروجی: "پیام به info@clinic.ir ارسال شد."
  • معماری اختیاری (Opt-in): کلاس EmailDetector در فاز ۱۲ به صورت اختیاری ارائه شده و در مجموعهٔ پیش‌فرض قرار ندارد. ایمیل‌هایی با بخش محلی عددی (مانند 09123456789@example.com) ممکن است با تشخیص‌دهنده‌های موبایل یا کد ملی همپوشانی ایجاد کنند. از آنجا که redact() و pseudonymize() در صورت وجود همپوشانی خطا می‌دهند، فعال‌سازی ایمیل تا زمان پیاده‌سازی مکانیزم حل تعارض به صورت صریح و اختیاری خواهد بود.
  • پیکربندی تشخیص‌دهنده‌ها در هر فراخوانی (Per-Call Detectors): کلاس PseudonymizationSession وضعیت نگاشت و شمارنده‌های نام‌مستعارسازی را نگه می‌دارد، اما مجموعهٔ تشخیص‌دهنده‌ها را در خود ذخیره نمی‌کند. تشخیص‌دهنده‌ها در سازندهٔ PseudonymizationSession تنظیم نمی‌شوند و برای هر فراخوانی pseudonymize() باید در صورت نیاز آرگومان detectors=[...] به‌صورت صریح ارسال شود.
  • قالب استاندارد اسکی (Conservative ASCII): اعتبارسنجی بخش محلی dot-atom (حداکثر ۶۴ کاراکتر)، نام دامنهٔ ساختاریافته مطابق DNS (حداکثر ۲۵۳ کاراکتر دامنه، ۱ تا ۶۳ کاراکتر برای هر برچسب، حداقل ۲ کاراکتر برای TLD و حداکثر ۲۵۴ کاراکتر برای کل آدرس).
  • قالب‌های پشتیبانی‌نشده: ساختارهای پیچیده یا منسوخ مانند رشته‌های کوتیشن‌دار ("john doe"@example.com)، دامنه‌های لیترال IP (user@[192.168.1.1])، کامنت‌ها، فاصله‌های شکسته‌شده (folding whitespace)، دامنه‌های تک‌بخشی (user@localhost) و ایمیل‌های بین‌المللی غیر اسکی (EAI / RFC 6530+) پذیرفته نمی‌شوند.
  • سلب مسئولیت و حریم خصوصی: تابع is_valid_email صرفاً ساختار نگارشی را به صورت محلی و آفلاین بررسی می‌کند. این تابع هیچ‌گونه درخواست شبکه، استعلام DNS یا بررسی وجود صندوق پستی (Mailbox) انجام نمی‌دهد و هیچ داده‌ای را لاگ نمی‌کند. صحت ساختاری به منزلهٔ وجود واقعی آدرس ایمیل نیست.

۷. اعتبارسنجی و تشخیص شماره کارت بانکی / PAN (اختیاری)

کتابخانهٔ fa-redact تابع اعتبارسنجی مستقل is_valid_bank_card_number و تشخیص‌دهندهٔ BankCardDetector را برای شماره‌های ۱۶ رقمی کارت‌های پرداخت بانکی (Primary Account Number / PAN) به صورت کاملاً آفلاین و قطعی ارائه می‌دهد:

from fa_redact import (
    BankCardDetector,
    PseudonymizationSession,
    detect,
    is_valid_bank_card_number,
    redact,
)

# ۱. اعتبارسنجی مستقل (دقیقاً ۱۶ رقم، الگوریتم چکسام Luhn):
is_valid_bank_card_number("1234567890123452")  # True (کارت معتبر آزمایشی)
is_valid_bank_card_number("۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۲")  # True (پشتیبانی از ارقام فارسی)
is_valid_bank_card_number("١٢٣٤٥٦٧٨٩٠١٢٣٤٥٢")  # True (پشتیبانی از ارقام عربی)
is_valid_bank_card_number("1234 5678 9012 3452")  # False (رد فاصله)
is_valid_bank_card_number("1234-5678-9012-3452")  # False (رد خط تیره)
is_valid_bank_card_number("0000000000000000")  # False (رد رشته‌های تماماً یکسان)
is_valid_bank_card_number("1234567890123453")  # False (عدم تطابق چکسام)

# ۲. تشخیص با فعال‌سازی اختیاری (Opt-in):
text = "شماره کارت: 1234567890123452"
detections = detect(text, detectors=[BankCardDetector()])
# خروجی: [Detection(type='BANK_CARD', value='1234567890123452', ...)]

# ۳. پنهان‌سازی و نام‌مستعارسازی اختیاری:
redacted_text = redact(text, detectors=[BankCardDetector()])
# خروجی: "شماره کارت: [BANK_CARD_1]"

session = PseudonymizationSession()
pseudonymized = session.pseudonymize(
    "واریز به کارت ۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۲ انجام شد.",
    detectors=[BankCardDetector()],
)
# خروجی: "واریز به کارت [BANK_CARD_1] انجام شد."

restored = session.restore("تایید واریز به [BANK_CARD_1]")
# خروجی: "تایید واریز به ۱۲۳۴۵۶۷۸۹۰۱۲۳۴۵۲"
  • معماری اختیاری (Opt-in): کلاس BankCardDetector به صورت اختیاری ارائه شده و در مجموعهٔ پیش‌فرض (_DEFAULT_DETECTORS) قرار ندارد.
  • الگوریتم چکسام Luhn (MOD-10): بررسی صحت شماره‌های ۱۶ رقمی کارت با الگوریتم استاندارد Luhn (دوبرابر کردن ارقام در جایگاه‌های فرد از راست، کسر ۹ در صورت بزرگ‌تر شدن از ۹ و بررسی sum % 10 == 0).
  • نرم‌سازی با حفظ موقعیت کاراکتری: پشتیبانی از ارقام لاتین (0-9)، فارسی (۰-۹) و عربی (٠-٩). حفظ شکل ظاهری در Detection.value و تبدیل به ارقام استاندارد اسکی در Detection.normalized_value.
  • فیلتر دفاعی رشته‌های تکراری: رشته‌های ۱۶ رقمی تماماً تکراری (0000000000000000 تا 9999999999999999) حتی در صورت تطابق ریاضی چکسام، رد می‌شوند.
  • قالب فشردهٔ الکترونیکی: صرفاً رشته‌های ۱۶ رقمی پیوسته بدون فاصله، خط تیره یا جداکننده پذیرفته می‌شوند.
  • بی‌طرفی نسبت به صادرکننده (Issuer Neutrality): شناسه‌ها و نام‌گذاری‌ها به صورت خنثی (BANK_CARD) طراحی شده‌اند. این کتابخانه پایگاه داده BIN/IIN بانکی نگهداری نمی‌کند و تعلقی به یک بانک ایرانی یا شبکهٔ خاص را تایید نمی‌کند.
  • سلب مسئولیت بانکی و امنیتی: معتبر بودن الگوریتم Luhn به معنی واقعی بودن کارت، فعال بودن آن، تعلق آن به بانک ایرانی، مالکیت آن توسط شخص مشخص یا وجود حساب مرتبط با آن نیست. این تابع صرفاً اعتبارسنجی محلی و آفلاین ریاضی انجام می‌دهد و هیچ‌گونه اتصال به درگاه پرداخت، استعلام CVV2 یا بررسی وضعیت حساب انجام نمی‌دهد.

۸. شناسه‌های سازمانی / درمانی قابل پیکربندی (اختیاری)

شماره پرونده پزشکی (MRN)، شناسه بیمار، شناسه پذیرش، شناسه مراجعه / Encounter، شناسه پرونده و سایر شناسه‌های درمانی و سازمانی کاملاً وابسته به سازمان یا بیمارستان مربوطه هستند. برای این شناسه‌ها قالب واحد و جهانی وجود ندارد و کتابخانهٔ fa-redact الگوهای حدسی یا پیش‌فرض برای آن‌ها تعریف نمی‌کند.

به جای آن، لایهٔ پیکربندی سبک، تغییرناپذیر و مستقلی (PatternRule و PatternDetector) ارائه شده است تا سازمان یا بیمارستان بتواند الگوهای منظم (Regex) اختصاصی خود را تعریف کند:

import re
from fa_redact import (
    PatternDetector,
    PatternRule,
    PseudonymizationSession,
    detect,
    redact,
)

# ۱. تعریف قواعد اختصاصی بیمارستان / سازمان
hospital_detector = PatternDetector(
    [
        # شماره پرونده پزشکی (MRN)
        PatternRule(
            type="MRN",
            pattern=r"(?<!\w)MRN-[0-9]{6}(?!\w)",
        ),
        # شناسه بیمار با استفاده از گروه نام‌دار جهت حفظ برچسب 'Patient ID: '
        PatternRule(
            type="PATIENT_ID",
            pattern=r"Patient\s*ID\s*:\s*(?P<id>PAT-[A-Z]{2}-[0-9]{8})",
            group="id",
            flags=re.IGNORECASE,
        ),
        # شناسه پذیرش
        PatternRule(
            type="ADMISSION_ID",
            pattern=r"(?<!\w)ADM-20[0-9]{2}-[0-9]{6}(?!\w)",
        ),
        # شناسه مراجعه / Encounter
        PatternRule(
            type="ENCOUNTER_ID",
            pattern=r"(?<!\w)ENC-[0-9]{10}(?!\w)",
        ),
    ]
)

# ۲. تشخیص بر روی متن با پشتیبانی از ارقام فارسی در متن اصلی
text = "پرونده: MRN-۱۲۳۴۵۶ و Patient ID: PAT-TE-12345678"
detections = detect(text, detectors=[hospital_detector])
# خروجی شامل:
# - Detection(type='MRN', value='MRN-۱۲۳۴۵۶', normalized_value='MRN-123456', span=[8:18])
# - Detection(type='PATIENT_ID', value='PAT-TE-12345678', normalized_value='PAT-TE-12345678', span=[33:48])

# ۳. پنهان‌سازی (حفظ برچسب متن 'Patient ID: ' و جایگزینی فقط بخش شناسه)
redacted = redact(text, detectors=[hospital_detector])
# خروجی: "پرونده: [MRN_1] و Patient ID: [PATIENT_ID_1]"

# ۴. نام‌مستعارسازی چندمرحله‌ای با ارقام فارسی و انگلیسی
session = PseudonymizationSession()
# مرحله اول: با ارقام فارسی
turn1 = session.pseudonymize("پرونده: MRN-۱۲۳۴۵۶", detectors=[hospital_detector])
# خروجی: "پرونده: [MRN_1]"

# مرحله دوم: با ارقام لاتین (به همان [MRN_1] متصل می‌شود)
turn2 = session.pseudonymize("پیگیری پرونده MRN-123456", detectors=[hospital_detector])
# خروجی: "پیگیری پرونده [MRN_1]"

# بازگردانی به اولین شکل دیده‌شده (ارقام فارسی):
restored = session.restore("پاسخ به [MRN_1]")
# خروجی: "پاسخ به MRN-۱۲۳۴۵۶"
  • معماری اختیاری (Opt-in): کلاس PatternDetector کاملاً اختیاری است و در _DEFAULT_DETECTORS قرار ندارد. ارسال detectors=[hospital_detector] تشخیص‌دهنده‌های پیش‌فرض را برای آن فراخوانی جایگزین می‌کند.
  • تطابق روی متن نرمال‌شده (پیش‌فرض): در حالت پیش‌فرض (source="normalized"), الگوها با متن نرمال‌شده مطابقت داده می‌شوند؛ بنابراین یک رجکس ساده اسکی مانند MRN-[0-9]{6} می‌تواند ارقام فارسی (MRN-۱۲۳۴۵۶)، عربی (MRN-١٢٣٤٥٦) و لاتین (MRN-123456) را تشخیص دهد و فرم ظاهری را در Detection.value و مقدار نرمال‌شده را در Detection.normalized_value حفظ نماید.
  • حالت تطابق متن خام (source="original"): در صورت نیاز به بررسی دقیق بر روی متن دست‌نخورده، حالت source="original" نیز پشتیبانی می‌شود.
  • پشتیبانی از گروه‌های انتخابی (Capture Groups): امکان استفاده از اندیس عددی (group=1) یا گروه نام‌دار (group="id") برای جدا کردن شناسه از کلمات مجاور یا برچسب‌ها. عدم شرکت گروه در تطابق با ValueError مواجه خواهد شد.
  • مالکیت مرزهای رجکس (Boundary Ownership): کتابخانه رجکس‌های تعریف‌شده توسط کاربر را در مرزهای کلمه مانند \b محصور نمی‌کند؛ کنترل کامل رفتار رجکس بر عهدهٔ سازمان پیکربندی‌کننده است.
  • کامپایل یکباره و تغییرناپذیری: کلاس PatternRule یک dataclass منجمد (frozen) و تغییرناپذیر است و الگوها در زمان ساخت PatternDetector یک‌بار کامپایل و کش می‌شوند.

۹. حل صریح تعارض تشخیص‌ها (اختیاری)

تابع detect() همواره شواهد خام تشخیص‌دهنده‌ها را بازمی‌گرداند و همپوشانی‌ها و رکوردهای تکراری را دقیقاً حفظ می‌کند تا امکان ممیزی و بازبینی شفاف وجود داشته باشد.

به‌صورت پیش‌فرض، توابع redact() و session.pseudonymize() با سیاست محافظه‌کارانهٔ conflict_policy="reject" اجرا می‌شوند و در صورت وجود هرگونه تداخل، همپوشانی یا تکرار، با خطای ValueError متوقف می‌شوند:

from fa_redact import BankCardDetector, EmailDetector, redact

text = "ایمیل: 1234567890123452@example.com"
detectors = [EmailDetector(), BankCardDetector()]

# سیاست پیش‌فرض reject: به دلیل تداخل spanهای EMAIL و BANK_CARD خطا می‌دهد
# redact(text, detectors=detectors)  # -> ValueError

در صورت نیاز، سازمان یا برنامه می‌تواند با انتخاب یکی از سیاست‌های صریح، نحوهٔ حل تعارض را مشخص کند:

  • سیاست طولانی‌ترین بازه (conflict_policy="longest"): به‌صورت حریصانه طولانی‌ترین span را انتخاب کرده و تشخیص‌های کوتاه‌تر همپوشان را حذف می‌کند. موارد کاملاً تکراری در یکدیگر ادغام می‌شوند. در صورت وجود همپوشانی مبهم با طول مساوی، ValueError صادر می‌شود.
  • سیاست اولویت نوع موجودیت (conflict_policy="priority"): اولویت‌بندی بر اساس فهرست مشخص‌شده در type_priority=[...] انجام می‌شود و موجودیت با اولویت بالاتر برنده خواهد بود (حتی اگر طول span آن کوتاه‌تر باشد). تمامی انواع موجودیت‌های درگیر در تعارض باید در type_priority ذکر شده باشند.
# انتخاب EMAIL به دلیل طول بیشتر
redacted_longest = redact(text, detectors=detectors, conflict_policy="longest")
# خروجی: "ایمیل: [EMAIL_1]"

# اولویت‌دهی به کارت بانکی بر روی ایمیل
redacted_priority = redact(
    text,
    detectors=detectors,
    conflict_policy="priority",
    type_priority=["BANK_CARD", "EMAIL"],
)
# خروجی: "ایمیل: [BANK_CARD_1]@example.com"

۱۰. بازسازی دقیق بر اساس span در پنهان‌سازی

خروجی بر اساس بازه‌های دقیق Detection از متن اصلی ساخته می‌شود؛ فقط همان spanهای تشخیص‌داده‌شده جایگزین می‌شوند و بخش‌های دیگر متن بدون تغییر کپی می‌شوند. پیاده‌سازی از str.replace() سراسری بر اساس مقدار استفاده نمی‌کند.

۱۱. ویژگی‌های امنیتی و رفتاری نشست نام‌مستعارسازی

کلاس PseudonymizationSession رفتارهای امنیتی زیر را تضمین می‌کند:

  • ایزوله‌سازی نشست‌ها (Mapping Isolation): هر نشست دارای حافظه و نگاشت مستقل است.
  • یکپارچگی هویت در چند مرحله (Cross-Call Identity): هویت یک موجودیت بر اساس (type, normalized_value) ردیابی می‌شود. بنابراین ۰۹۱۲۳۴۵۶۷۸۹ در نوبت اول و 09123456789 در نوبت دوم هر دو به [IR_MOBILE_1] متصل می‌شوند.
  • محدودیت تفاوت فرمت داخلی و بین‌المللی: مقادیر 09123456789 و +989123456789 دارای رشته‌های نرمال‌شدهٔ متفاوتی هستند و در نسخهٔ v0.2.0 به یک هویت یکسان تبدیل نمی‌شوند.
  • بازگردانی به اولین نمایش دیده‌شده (First-Observed Representative): تابع restore() یک بازگردانی معنایی بر اساس placeholder انجام می‌دهد و تضمین نمی‌کند که شکل نویسه‌ای دقیق تک‌تک occurrenceهای متن اولیه به‌صورت byte-for-byte بازسازی شود.
  • بازگردانی غیرآبشاری (Non-Cascading Restore): بازگردانی در یک مرحله و با فرار کاراکترهای خاص انجام می‌شود تا اگر مقدار بازگردانده‌شده شبیه به نشان‌گذار باشد، بازگردانی بازگشتی و ناخواسته رخ ندهد.
  • به‌روزرسانی اتمیک (Atomic Updates): اگر در حین پردازش خطایی رخ دهد، وضعیت نشست و شمارنده‌ها دست‌نخورده باقی می‌مانند.
  • محافظت در برابر تداخل تاریخی با نشان‌گذارهای متنی: اگر در متون قبلی عبارتی مانند [IR_MOBILE_2] به عنوان متن عادی وجود داشته باشد، سیستم آن شماره را رزرو کرده و برای شناسه‌های واقعی جدید اختصاص نمی‌دهد.
  • عدم دستکاری نشان‌گذارهای ناشناخته: نشان‌گذارهایی که در نگاشت نشست ثبت نشده‌اند (مانند [IR_MOBILE_99]) بدون خطا و دست‌نخورده باقی می‌مانند.

تشخیص‌دهنده‌های سفارشی (Custom Detectors)

معماری fa-redact مبتنی بر پروتکل‌های ساختاری پایتون (Duck Typing) است. شما می‌توانید کلاسی با متد دوآرگومانی پیاده‌سازی کنید:

import re
from collections.abc import Sequence

from fa_redact import Detection, detect


class MedicalRecordNumberDetector:
    """نمونه تشخیص‌دهنده سفارشی برای شماره پرونده پزشکی بیمارستانی (MRN)."""

    def detect(
        self,
        original_text: str,
        normalized_text: str,
    ) -> Sequence[Detection]:
        detections: list[Detection] = []

        for match in re.finditer(r"\bMRN-\d{6}\b", normalized_text):
            detections.append(
                Detection.from_texts(
                    type="MRN",
                    original_text=original_text,
                    normalized_text=normalized_text,
                    start=match.start(),
                    end=match.end(),
                )
            )

        return detections


# توجه: پاس دادن فهرست اختصاصی تشخیص‌دهنده‌ها، مجموعهٔ پیش‌فرض را برای آن فراخوانی جایگزین می‌کند.
text = "پرونده با MRN-123456 ثبت شد."
detections = detect(text, detectors=[MedicalRecordNumberDetector()])

در این پروتکل:

  • original_text: متن اصلی و دست‌نخورده است.
  • normalized_text: نسخهٔ نرمال‌شده با طول دقیقاً برابر است.
  • تشخیص‌دهنده باید spanها را طوری برگرداند که offsetها در هر دو رشته یکسان باشند (استفاده از Detection.from_texts() این موضوع را تضمین می‌کند).
  • پاس دادن detectors=[...] مجموعهٔ تشخیص‌دهنده‌های پیش‌فرض را برای آن فراخوانی جایگزین می‌کند و به‌طور خودکار به آن‌ها افزوده نمی‌شود.

کاربرد در حوزهٔ سلامت و هوش مصنوعی (Healthcare & AI/LLM)

محیط محلی و امن بیمارستان / سازمان
  │
  ├── ۱. متن پرونده حاوی شناسه‌های حساس بیمار
  │      ↓
  ├── ۲. session.pseudonymize(raw_text)
  │      ↓
  ├── ۳. پرامپت نام‌مستعارسازی‌شده (فقط شناسه‌های پشتیبانی‌شده جایگزین شده‌اند)
  │
  ▼  (ارسال صرفاً متن پالایش‌شده به هوش مصنوعی خارج از سازمان)
سرویس مدل زبانی بزرگ (LLM API)
  ▲
  │  (دریافت پاسخ شامل نشان‌گذارها)
  ▼
محیط محلی و امن بیمارستان / سازمان
  │
  └── ۴. session.restore(llm_response)
         ↓
      ۵. گزارش نهایی با شناسه‌های واقعی و بازگردانی‌شده

جدول پوشش و قابلیت‌ها

نوع شناسه هویتی / قابلیت v0.1.0 v0.2.0 توضیحات
کد ملی ایران ✅ پشتیبانی می‌شود ✅ پیش‌فرض اعتبارسنجی دقیق ۱۰ رقمی با قاعدهٔ چکسام Modulo-11
شماره تلفن همراه ایران ✅ پشتیبانی می‌شود ✅ پیش‌فرض اعتبارسنجی پیش‌شماره‌های مصوب رگولاتوری ایران (CRA 2026)
شماره شبا (IBAN) ❌ پشتیبانی نمی‌شود ✅ پیش‌فرض اعتبارسنجی دقیق ۲۶ کاراکتری با قاعدهٔ چکسام MOD-97 (IR + ۲۴ رقم)
آدرس ایمیل ❌ پشتیبانی نمی‌شود 🧪 اختیاری اعتبارسنجی و تشخیص ایمیل‌های اسکی محافظه‌کارانه (detectors=[EmailDetector()])
شماره کارت بانکی (PAN) ❌ پشتیبانی نمی‌شود 🧪 اختیاری فرمت فشردهٔ ۱۶ رقمی + Luhn؛ بدون استعلام BIN/IIN یا صادرکننده (detectors=[BankCardDetector()])
شناسه‌های سازمانی / درمانی (MRN و بیمار) ❌ پشتیبانی نمی‌شود 🧪 اختیاری قابل پیکربندی اختصاصی توسط کاربر با PatternRule و PatternDetector
حل صریح تعارض تشخیص‌ها ❌ پشتیبانی نمی‌شود 🧪 سیاست اختیاری حل همپوشانی‌ها و تکرارها با سیاست "longest" یا "priority"
نام اشخاص ❌ پشتیبانی نمی‌شود ❌ پشتیبانی نمی‌شود نیازمند مدل‌های پردازش زبان طبیعی و بازشناسی موجودیت‌های نام‌دار (NER)
آدرس پستی و موقعیت مکانی ❌ پشتیبانی نمی‌شود ❌ پشتیبانی نمی‌شود موجودیت‌های غیرساختاریافته
تاریخ تولد و زمان‌ها ❌ پشتیبانی نمی‌شود ❌ پشتیبانی نمی‌شود برنامه‌ریزی‌شده برای نسخه‌های آتی
شماره بیمه درمانی ❌ پشتیبانی نمی‌شود ❌ پشتیبانی نمی‌شود فرمت سازمانی

مدل حریم خصوصی و سلب مسئولیت‌های امنیتی


سیاست داده‌های آزمایشی امن

کلیهٔ مثال‌ها، داک‌ها و تست‌های موجود در این مخزن بر پایهٔ داده‌های مصنوعی، الگوهای الگوریتمی و بردارهای تستی غیرشخصی تولید شده‌اند. هیچ‌گونه اطلاعات واقعی هویتی، پرونده پزشکی، یا داده‌های محرمانهٔ کاربران در این مخزن استفاده یا ذخیره نشده است.


توسعه و کنترل کیفیت

اجرای تست‌های خودکار:

python -m pytest

بررسی سبک کدنویسی و لینتینگ:

ruff check .
ruff format --check .

بررسی ایستا و تایپ‌چکینگ:

mypy src
mypy tests

بیلد پکیج و بررسی صحت توزیع‌ها:

python -m build
python -m twine check dist/*

برای آگاهی از نحوهٔ مشارکت در پروژه، راهنماهای CONTRIBUTING.md و SECURITY.md را مطالعه فرمایید.


مجوز (License)

این پروژه تحت مجوز MIT منتشر شده است.

Metadata

Release files for fa-redact 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fa-redact 0.2.0
File Size Uploaded
fa_redact-0.2.0.tar.gz 136.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fa-redact 0.2.0
File Interpreter ABI Platform
fa_redact-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 190.8 kB

Release files / fa_redact-0.2.0.tar.gz

Download URL fa_redact-0.2.0.tar.gz
Size 136.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a1fc96f808386522d41c6332d96d4ca80a6f3f23de118aeb67d970f75a1521a7
BLAKE2b-256 checksum
How to use checksums
45ac0631b386efc858fa8c823dfb912c524c1980be2b997d53e11f9c1cd75238
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release files / fa_redact-0.2.0-py3-none-any.whl

Download URL fa_redact-0.2.0-py3-none-any.whl
Size 54.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b0af860dd7d96583f7d7794ea704152ecd9b1dc50d1a15aabde4e88785ab0c53
BLAKE2b-256 checksum
How to use checksums
339653c3de6ec5cdfcd491a07da09edb36946ac61469d038474d835ff7e5d0b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.0

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page