fredwebx commited on
Commit
c6d4e82
·
verified ·
1 Parent(s): 9c73107

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +269 -0
app.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ from faker import Faker
4
+ import io
5
+ import os
6
+ import re
7
+ import json
8
+ from typing import Optional
9
+
10
+ # ── Example CSV (created at startup, before Blocks definition) ────────────────
11
+ def _create_example():
12
+ if os.path.exists("example.csv"):
13
+ return
14
+ sample = pd.DataFrame({
15
+ "id": range(1, 6),
16
+ "first_name": ["Mario", "Lucia", "Giovanni", "Sofia", "Marco"],
17
+ "last_name": ["Rossi", "Bianchi", "Ferrari", "Esposito", "Romano"],
18
+ "email": ["mario.rossi@gmail.com", "lucia.b@yahoo.it", "g.ferrari@outlook.com",
19
+ "sofia.e@libero.it", "m.romano@hotmail.com"],
20
+ "phone": ["+39 333 1234567", "02 9876543", "+39 347 9988776",
21
+ "080 5551234", "+39 320 4567890"],
22
+ "city": ["Roma", "Milano", "Napoli", "Bari", "Torino"],
23
+ "notes": ["premium customer", "newsletter yes", "B2B", "trial", "enterprise"],
24
+ })
25
+ sample.to_csv("example.csv", index=False)
26
+
27
+ _create_example()
28
+
29
+ fake = Faker(["it_IT", "en_US"])
30
+ Faker.seed(0)
31
+
32
+ # ── Available anonymization types ─────────────────────────────────────────────
33
+ ANON_TYPES = {
34
+ "email": "📧 Email",
35
+ "first_name": "👤 First Name",
36
+ "last_name": "👤 Last Name",
37
+ "full_name": "👤 Full Name",
38
+ "phone": "📱 Phone",
39
+ "address": "🏠 Address",
40
+ "city": "🏙️ City",
41
+ "postal_code": "📮 Postal Code",
42
+ "tax_id": "🪪 Tax ID",
43
+ "date_of_birth": "📅 Date of Birth",
44
+ "generic": "🔒 Generic",
45
+ }
46
+
47
+ TYPE_LABELS = {v: k for k, v in ANON_TYPES.items()} # label → internal key
48
+
49
+ # ── Automatic type detection ──────────────────────────────────────────────────
50
+ def _detect_type(series: pd.Series) -> str:
51
+ name = (series.name or "").lower()
52
+ sample = series.dropna().astype(str).head(200)
53
+
54
+ # Column-name heuristics
55
+ if any(k in name for k in ("email", "mail", "e-mail")):
56
+ return "email"
57
+ if any(k in name for k in ("telefon", "phone", "cell", "mobile", "tel")):
58
+ return "phone"
59
+ if any(k in name for k in ("full_name", "fullname", "nome_completo")):
60
+ return "full_name"
61
+ if any(k in name for k in ("nome", "first", "given")):
62
+ return "first_name"
63
+ if any(k in name for k in ("cognome", "surname", "last")):
64
+ return "last_name"
65
+ if any(k in name for k in ("indirizzo", "address", "via", "street")):
66
+ return "address"
67
+ if any(k in name for k in ("città", "city", "comune", "citta")):
68
+ return "city"
69
+ if any(k in name for k in ("cap", "postal", "zip")):
70
+ return "postal_code"
71
+ if any(k in name for k in ("cf", "codice_fiscale", "fiscal", "tax")):
72
+ return "tax_id"
73
+ if any(k in name for k in ("nascita", "birth", "dob", "birthday")):
74
+ return "date_of_birth"
75
+
76
+ # Content-based heuristics
77
+ if sample.str.match(r"^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$").mean() > 0.5:
78
+ return "email"
79
+ if sample.str.match(r"^[\+\d\s\-\(\)]{7,15}$").mean() > 0.5:
80
+ return "phone"
81
+
82
+ return "generic"
83
+
84
+ # ── Fake-data generators (deterministic per value) ────────────────────────────
85
+ def _make_fake(value: str, anon_type: str, cache: dict) -> str:
86
+ if value in cache:
87
+ return cache[value]
88
+
89
+ # Deterministic seed: same value → same fake output
90
+ seed = abs(hash(value)) % (2**32)
91
+ fake_local = Faker(["it_IT", "en_US"])
92
+ fake_local.seed_instance(seed)
93
+
94
+ generators = {
95
+ "email": lambda: fake_local.email(),
96
+ "first_name": lambda: fake_local.first_name(),
97
+ "last_name": lambda: fake_local.last_name(),
98
+ "full_name": lambda: fake_local.name(),
99
+ "phone": lambda: fake_local.phone_number(),
100
+ "address": lambda: fake_local.street_address(),
101
+ "city": lambda: fake_local.city(),
102
+ "postal_code": lambda: fake_local.postcode(),
103
+ "tax_id": lambda: _fake_tax_id(fake_local),
104
+ "date_of_birth": lambda: fake_local.date_of_birth(minimum_age=18, maximum_age=80).strftime("%d/%m/%Y"),
105
+ "generic": lambda: f"ANONYMIZED_{abs(hash(value)) % 100000:05d}",
106
+ }
107
+
108
+ result = generators.get(anon_type, generators["generic"])()
109
+ cache[value] = result
110
+ return result
111
+
112
+ def _fake_tax_id(f: Faker) -> str:
113
+ """Generate a plausible 16-char alphanumeric tax ID (Italian CF format)."""
114
+ letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
115
+ digits = "0123456789"
116
+ tax_id = (
117
+ "".join(f.random_choices(letters, length=3))
118
+ + "".join(f.random_choices(letters, length=3))
119
+ + "".join(f.random_choices(digits, length=2))
120
+ + f.random_choices(letters, length=1)[0]
121
+ + "".join(f.random_choices(digits, length=2))
122
+ + f.random_choices(letters, length=1)[0]
123
+ + "".join(f.random_choices(digits, length=3))
124
+ + f.random_choices(letters, length=1)[0]
125
+ )
126
+ return tax_id
127
+
128
+ # ── Main logic ────────────────────────────────────────────────────────────────
129
+ def load_csv(file_obj) -> tuple:
130
+ """Load the CSV and return preview + column configuration."""
131
+ if file_obj is None:
132
+ return None, None, gr.update(visible=False), "Upload a CSV file to get started."
133
+
134
+ # Gradio 5: file_obj is a path string; Gradio 4: file_obj.name
135
+ file_path = file_obj if isinstance(file_obj, str) else file_obj.name
136
+
137
+ try:
138
+ df = pd.read_csv(file_path, encoding="utf-8", on_bad_lines="skip")
139
+ except UnicodeDecodeError:
140
+ df = pd.read_csv(file_path, encoding="latin-1", on_bad_lines="skip")
141
+ except Exception as e:
142
+ return None, None, gr.update(visible=False), f"Error reading the file: {e}"
143
+
144
+ if df.empty or len(df.columns) == 0:
145
+ return None, None, gr.update(visible=False), "The CSV file looks empty or malformed."
146
+
147
+ # Build column configuration (name, detected type, selected)
148
+ col_config = pd.DataFrame({
149
+ "Column": df.columns.tolist(),
150
+ "Type": [ANON_TYPES[_detect_type(df[c])] for c in df.columns],
151
+ "Anonymize": [True] * len(df.columns),
152
+ })
153
+
154
+ msg = f"✅ Loaded: **{len(df)} rows × {len(df.columns)} columns**. Review the detected types and click **Anonymize**."
155
+ return df.to_json(orient="split"), col_config, gr.update(visible=True), msg
156
+
157
+
158
+ def anonymize(df_json: Optional[str], col_config_df: Optional[pd.DataFrame]) -> tuple:
159
+ """Anonymize the DataFrame according to configuration."""
160
+ if df_json is None:
161
+ return None, None, "Please upload a CSV first."
162
+ if col_config_df is None or len(col_config_df) == 0:
163
+ return None, None, "Column configuration is missing."
164
+
165
+ df = pd.read_json(io.StringIO(df_json), orient="split")
166
+ result = df.copy()
167
+
168
+ processed = 0
169
+ for _, row in col_config_df.iterrows():
170
+ col_name = row["Column"]
171
+ do_anon = row["Anonymize"]
172
+ type_label = row["Type"]
173
+
174
+ if not do_anon or col_name not in result.columns:
175
+ continue
176
+
177
+ anon_type = TYPE_LABELS.get(type_label, "generic")
178
+ cache: dict = {}
179
+
180
+ result[col_name] = result[col_name].apply(
181
+ lambda v: _make_fake(str(v), anon_type, cache) if pd.notna(v) and str(v).strip() != "" else v
182
+ )
183
+ processed += 1
184
+
185
+ # Export to CSV
186
+ csv_bytes = result.to_csv(index=False).encode("utf-8")
187
+ csv_path = "/tmp/anonymized.csv"
188
+ with open(csv_path, "wb") as f:
189
+ f.write(csv_bytes)
190
+
191
+ msg = f"✅ Anonymized **{processed} columns** out of {len(df.columns)} total."
192
+ return result.head(10), csv_path, msg
193
+
194
+ # ── UI ────────────────────────────────────────────────────────────────────────
195
+ DESCRIPTION = """
196
+ # 🔒 CSV Data Anonymizer — GDPR Ready
197
+
198
+ Upload a CSV, review the columns detected as sensitive, and download the anonymized version.
199
+ The mapping is **deterministic**: the same value always produces the same fake data, preserving dataset consistency.
200
+ """
201
+
202
+ with gr.Blocks(theme=gr.themes.Soft(), title="Data Anonymizer") as demo:
203
+ # Internal state
204
+ df_state = gr.State(None)
205
+
206
+ gr.Markdown(DESCRIPTION)
207
+
208
+ with gr.Row():
209
+ with gr.Column(scale=1):
210
+ file_input = gr.File(
211
+ label="📂 Upload CSV",
212
+ file_types=[".csv"],
213
+ type="filepath",
214
+ )
215
+ status_box = gr.Markdown("Upload a CSV file to get started.")
216
+
217
+ with gr.Column(scale=2):
218
+ col_editor = gr.Dataframe(
219
+ label="⚙️ Column configuration (edit Type and Anonymize)",
220
+ headers=["Column", "Type", "Anonymize"],
221
+ datatype=["str", "str", "bool"],
222
+ col_count=(3, "fixed"),
223
+ visible=False,
224
+ interactive=True,
225
+ wrap=True,
226
+ )
227
+
228
+ anon_btn = gr.Button("🔒 Anonymize", variant="primary", size="lg")
229
+
230
+ gr.Markdown("### Result preview (first 10 rows)")
231
+ preview_out = gr.Dataframe(label="Anonymized preview", interactive=False, wrap=True)
232
+
233
+ download_out = gr.File(label="⬇️ Download anonymized CSV", visible=False)
234
+ result_status = gr.Markdown("")
235
+
236
+ # ── Event handlers ──────────────────────────────────────────────────────
237
+ file_input.change(
238
+ fn=load_csv,
239
+ inputs=[file_input],
240
+ outputs=[df_state, col_editor, col_editor, status_box],
241
+ )
242
+
243
+ def run_anonymize(df_json, col_config):
244
+ preview, path, msg = anonymize(df_json, col_config)
245
+ file_update = gr.update(value=path, visible=path is not None)
246
+ return preview, file_update, msg
247
+
248
+ anon_btn.click(
249
+ fn=run_anonymize,
250
+ inputs=[df_state, col_editor],
251
+ outputs=[preview_out, download_out, result_status],
252
+ )
253
+
254
+ gr.Examples(
255
+ examples=[["example.csv"]],
256
+ inputs=[file_input],
257
+ label="📋 Try with the example CSV",
258
+ run_on_click=False,
259
+ )
260
+
261
+ gr.Markdown(
262
+ """
263
+ ---
264
+ *The file is processed entirely in memory — no data is saved to disk or transmitted to third parties.*
265
+ """
266
+ )
267
+
268
+ if __name__ == "__main__":
269
+ demo.launch()