streamlit-mini-toolkit 0.1.0__py3-none-any.whl

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.
Files changed (42) hide show
  1. mini_toolkit/__init__.py +3 -0
  2. mini_toolkit/__main__.py +3 -0
  3. mini_toolkit/app.py +111 -0
  4. mini_toolkit/cli.py +40 -0
  5. mini_toolkit/core/__init__.py +1 -0
  6. mini_toolkit/core/net.py +56 -0
  7. mini_toolkit/core/pdf.py +12 -0
  8. mini_toolkit/core/registry.py +75 -0
  9. mini_toolkit/core/ui.py +90 -0
  10. mini_toolkit/streamlit_config.toml +16 -0
  11. mini_toolkit/style/style.css +171 -0
  12. mini_toolkit/tools/__init__.py +1 -0
  13. mini_toolkit/tools/background_remover.py +65 -0
  14. mini_toolkit/tools/change_tracker.py +90 -0
  15. mini_toolkit/tools/chat_alert.py +42 -0
  16. mini_toolkit/tools/color_picker.py +40 -0
  17. mini_toolkit/tools/document_converter.py +121 -0
  18. mini_toolkit/tools/file_compressor.py +78 -0
  19. mini_toolkit/tools/github_trending.py +53 -0
  20. mini_toolkit/tools/hash_generator.py +43 -0
  21. mini_toolkit/tools/image_converter.py +63 -0
  22. mini_toolkit/tools/image_downloader.py +67 -0
  23. mini_toolkit/tools/image_enhancer.py +54 -0
  24. mini_toolkit/tools/image_watermarker.py +63 -0
  25. mini_toolkit/tools/ocr.py +93 -0
  26. mini_toolkit/tools/page_speed.py +45 -0
  27. mini_toolkit/tools/password_checker.py +53 -0
  28. mini_toolkit/tools/pdf_tools.py +83 -0
  29. mini_toolkit/tools/qr_code.py +83 -0
  30. mini_toolkit/tools/resume_builder.py +385 -0
  31. mini_toolkit/tools/seo_health.py +64 -0
  32. mini_toolkit/tools/signature.py +72 -0
  33. mini_toolkit/tools/ssl_checker.py +68 -0
  34. mini_toolkit/tools/text_to_speech.py +34 -0
  35. mini_toolkit/tools/text_utilities.py +36 -0
  36. mini_toolkit/tools/url_shortener.py +29 -0
  37. mini_toolkit/tools/website_status.py +39 -0
  38. streamlit_mini_toolkit-0.1.0.dist-info/METADATA +151 -0
  39. streamlit_mini_toolkit-0.1.0.dist-info/RECORD +42 -0
  40. streamlit_mini_toolkit-0.1.0.dist-info/WHEEL +4 -0
  41. streamlit_mini_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  42. streamlit_mini_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,385 @@
1
+ import io
2
+ import os
3
+ import tempfile
4
+
5
+ import streamlit as st
6
+ from docx import Document
7
+ from docx.shared import Inches
8
+ from fpdf import FPDF, XPos, YPos
9
+
10
+ from core.pdf import pdf_safe
11
+ from core.ui import sanitize_filename
12
+
13
+ DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
14
+
15
+ # --- Helper function to generate DOCX resume ---
16
+ def generate_docx_resume(personal_info, experiences, educations, skills, profile_pic=None):
17
+ document = Document()
18
+
19
+ if profile_pic:
20
+ document.add_picture(profile_pic, width=Inches(1.5))
21
+
22
+ document.add_heading(personal_info['name'], level=1)
23
+ contact_p = document.add_paragraph()
24
+ contact_p.add_run(f"{personal_info['email']} | {personal_info['phone']} | {personal_info['linkedin']}")
25
+
26
+ if personal_info['summary']:
27
+ document.add_heading('Summary', level=2)
28
+ document.add_paragraph(personal_info['summary'])
29
+
30
+ if experiences:
31
+ document.add_heading('Experience', level=2)
32
+ for exp in experiences:
33
+ if exp['title'] and exp['company']:
34
+ p = document.add_paragraph()
35
+ p.add_run(exp['title']).bold = True
36
+ p.add_run(f" | {exp['company']}").italic = True
37
+ p.add_run(f"\n{exp['dates']}")
38
+ if exp['desc']:
39
+ document.add_paragraph(exp['desc'], style='List Bullet')
40
+
41
+ if educations:
42
+ document.add_heading('Education', level=2)
43
+ for edu in educations:
44
+ if edu['degree'] and edu['school']:
45
+ p = document.add_paragraph()
46
+ p.add_run(edu['degree']).bold = True
47
+ p.add_run(f" | {edu['school']}")
48
+ p.add_run(f"\n{edu['dates']}")
49
+
50
+ if skills:
51
+ document.add_heading('Skills', level=2)
52
+ document.add_paragraph(skills)
53
+
54
+ doc_buffer = io.BytesIO()
55
+ document.save(doc_buffer)
56
+ doc_buffer.seek(0)
57
+ return doc_buffer
58
+
59
+ # --- Helper function to generate PDF resume ---
60
+ class PDF(FPDF):
61
+ def header(self): pass
62
+ def footer(self): pass
63
+
64
+ def normalize_text(self, text):
65
+ return super().normalize_text(pdf_safe(text))
66
+
67
+ def generate_pdf_resume_classic(personal_info, experiences, educations, skills, profile_pic=None):
68
+ pdf = PDF('P', 'mm', 'A4')
69
+ pdf.add_page()
70
+ pdf.set_auto_page_break(auto=True, margin=15)
71
+ pdf.set_font('helvetica', '', 12)
72
+
73
+ # --- Header Section ---
74
+ if profile_pic:
75
+ # Layout WITH profile picture
76
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
77
+ profile_pic.seek(0)
78
+ tmp.write(profile_pic.read())
79
+ tmp_path = tmp.name
80
+
81
+ # Place image. Assuming W=H, image bottom is at y=12+33=45
82
+ pdf.image(tmp_path, x=15, y=12, w=33)
83
+ os.unlink(tmp_path) # Clean up the temp file
84
+
85
+ # Set cursor for text, to the right of the image
86
+ text_x_start = 15 + 33 + 5 # image_x + image_width + padding
87
+ pdf.set_xy(text_x_start, 18)
88
+ pdf.set_font('helvetica', 'B', 24)
89
+ pdf.cell(0, 10, personal_info['name'], align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
90
+
91
+ pdf.set_x(text_x_start) # Reset X to be next to image for the second line
92
+ pdf.set_font('helvetica', '', 12)
93
+ pdf.cell(0, 10, f"{personal_info['email']} | {personal_info['phone']} | {personal_info['linkedin']}", align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
94
+
95
+ pdf.set_y(50) # Set Y to be below the image for the next section
96
+ else:
97
+ # Layout WITHOUT profile picture (centered)
98
+ pdf.set_y(15)
99
+ pdf.set_font('helvetica', 'B', 24)
100
+ pdf.cell(0, 10, personal_info['name'], align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
101
+ pdf.set_font('helvetica', '', 12)
102
+ pdf.cell(0, 10, f"{personal_info['email']} | {personal_info['phone']} | {personal_info['linkedin']}", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
103
+ pdf.ln(5)
104
+
105
+ if personal_info['summary']:
106
+ pdf.set_font('helvetica', 'B', 14)
107
+ pdf.cell(0, 10, 'Summary', align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
108
+ pdf.set_font('helvetica', '', 12)
109
+ pdf.multi_cell(0, 5, personal_info['summary'])
110
+ pdf.ln(5)
111
+
112
+ if experiences:
113
+ pdf.set_font('helvetica', 'B', 14)
114
+ pdf.cell(0, 10, 'Experience', align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
115
+ for exp in experiences:
116
+ if exp['title'] and exp['company']:
117
+ pdf.set_font('helvetica', 'B', 12)
118
+ pdf.cell(0, 5, f"{exp['title']} | {exp['company']}", align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
119
+ pdf.set_font('helvetica', 'I', 12)
120
+ pdf.cell(0, 5, exp['dates'], align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
121
+ if exp['desc']:
122
+ # Using '*' instead of '•' to avoid Unicode errors with standard fonts
123
+ pdf.set_font('helvetica', '', 12)
124
+ pdf.multi_cell(0, 5, f" * {exp['desc']}")
125
+ pdf.ln(3)
126
+
127
+ if educations:
128
+ pdf.set_font('helvetica', 'B', 14)
129
+ pdf.cell(0, 10, 'Education', align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
130
+ for edu in educations:
131
+ if edu['degree'] and edu['school']:
132
+ pdf.set_font('helvetica', 'B', 12)
133
+ pdf.cell(0, 5, f"{edu['degree']} | {edu['school']}", align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
134
+ pdf.set_font('helvetica', 'I', 12)
135
+ pdf.cell(0, 5, edu['dates'], align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
136
+ pdf.ln(3)
137
+
138
+ if skills:
139
+ pdf.set_font('helvetica', 'B', 14)
140
+ pdf.cell(0, 10, 'Skills', align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
141
+ pdf.set_font('helvetica', '', 12)
142
+ pdf.multi_cell(0, 5, skills)
143
+
144
+ # Use pdf.output() which returns bytes directly, no need for manual encoding.
145
+ return io.BytesIO(pdf.output())
146
+
147
+ # --- Helper function to generate PDF resume (Modern Template) ---
148
+ def generate_pdf_resume_modern(personal_info, experiences, educations, skills):
149
+ pdf = PDF('P', 'mm', 'A4')
150
+ pdf.add_page()
151
+ pdf.set_auto_page_break(auto=True, margin=15)
152
+
153
+ # --- Theme Colors ---
154
+ pdf.set_text_color(40, 40, 40) # Dark Gray
155
+ heading_color = (0, 86, 179) # Blue
156
+
157
+ # --- Header ---
158
+ pdf.set_y(20)
159
+ pdf.set_font('helvetica', 'B', 32)
160
+ pdf.cell(0, 12, personal_info['name'], align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
161
+ pdf.set_font('helvetica', '', 11)
162
+ pdf.cell(0, 8, f"{personal_info['email']} | {personal_info['phone']} | {personal_info['linkedin']}", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
163
+ pdf.ln(8)
164
+ pdf.set_line_width(0.5)
165
+ pdf.set_draw_color(*heading_color)
166
+ pdf.line(x1=15, y1=pdf.get_y(), x2=195, y2=pdf.get_y())
167
+ pdf.ln(8)
168
+
169
+ # --- Helper to draw a section ---
170
+ def draw_section(title, content_func):
171
+ pdf.set_font('helvetica', 'B', 14)
172
+ pdf.set_text_color(*heading_color)
173
+ pdf.cell(0, 10, title.upper(), align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
174
+ pdf.set_text_color(40, 40, 40)
175
+ pdf.set_font('helvetica', '', 11)
176
+ content_func()
177
+ pdf.ln(6)
178
+
179
+ # --- Summary Section ---
180
+ if personal_info['summary']:
181
+ draw_section('Summary', lambda: pdf.multi_cell(0, 6, personal_info['summary']))
182
+
183
+ # --- Experience Section ---
184
+ if experiences:
185
+ def exp_content():
186
+ for exp in experiences:
187
+ if exp['title'] and exp['company']:
188
+ pdf.set_font('helvetica', 'B', 12)
189
+ pdf.cell(0, 6, exp['title'], align='L')
190
+ pdf.set_font('helvetica', '', 12)
191
+ pdf.cell(0, 6, exp['dates'], align='R', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
192
+
193
+ pdf.set_font('helvetica', 'I', 12)
194
+ pdf.cell(0, 6, exp['company'], align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
195
+
196
+ if exp['desc']:
197
+ pdf.set_font('helvetica', '', 11)
198
+ # Using '*' for bullet points for font compatibility
199
+ pdf.multi_cell(0, 5, f" * {exp['desc']}")
200
+ pdf.ln(4)
201
+ draw_section('Experience', exp_content)
202
+
203
+ # --- Education Section ---
204
+ if educations:
205
+ def edu_content():
206
+ for edu in educations:
207
+ if edu['degree'] and edu['school']:
208
+ pdf.set_font('helvetica', 'B', 12)
209
+ pdf.cell(130, 6, edu['degree'], align='L')
210
+ pdf.set_font('helvetica', '', 12)
211
+ pdf.cell(0, 6, edu['dates'], align='R', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
212
+
213
+ pdf.set_font('helvetica', 'I', 12)
214
+ pdf.cell(0, 6, edu['school'], align='L', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
215
+ pdf.ln(4)
216
+ draw_section('Education', edu_content)
217
+
218
+ # --- Skills Section ---
219
+ if skills:
220
+ def skills_content():
221
+ skill_list = [s.strip() for s in skills.split(',')]
222
+ # We'll just use a multi-cell for simplicity here
223
+ pdf.multi_cell(0, 6, ", ".join(skill_list))
224
+ draw_section('Skills', skills_content)
225
+
226
+ return io.BytesIO(pdf.output())
227
+
228
+
229
+ # --- Helper function to generate Cover Letter Text ---
230
+ def generate_cover_letter_text(personal_info, skills, job_title, company_name):
231
+ from datetime import datetime
232
+ today_date = datetime.today().strftime('%B %d, %Y')
233
+
234
+ # Try to pick a few relevant skills to highlight.
235
+ skills_list = [s.strip() for s in skills.split(',')]
236
+ highlight_skills = ""
237
+ if len(skills_list) > 2:
238
+ highlight_skills = f"my skills in {skills_list[0]}, {skills_list[1]}, and {skills_list[2]}"
239
+ elif skills_list:
240
+ highlight_skills = f"my skills in {', '.join(skills_list)}"
241
+ else:
242
+ highlight_skills = "my relevant skills"
243
+
244
+ cover_letter = f"""{personal_info['name']}
245
+ {personal_info['email']} | {personal_info['phone']}
246
+ {personal_info['linkedin']}
247
+
248
+ {today_date}
249
+
250
+ Hiring Manager
251
+ {company_name}
252
+
253
+ Dear Hiring Manager,
254
+
255
+ I am writing to express my enthusiastic interest in the {job_title} position at {company_name}. I am confident that my background and {highlight_skills} align perfectly with the requirements of this role.
256
+
257
+ My professional summary, "{personal_info['summary']}", reflects my dedication and experience. I am particularly excited about the opportunity to contribute to your team and believe my passion for technology and problem-solving would make me a valuable asset.
258
+
259
+ Thank you for considering my application. My resume is attached for your review, and I look forward to the possibility of discussing this exciting opportunity with you further.
260
+
261
+ Sincerely,
262
+ {personal_info['name']}
263
+ """
264
+ return cover_letter.strip()
265
+
266
+ # --- Helper function to generate Cover Letter DOCX ---
267
+ def generate_cover_letter_docx(cover_letter_text):
268
+ document = Document()
269
+ for para in cover_letter_text.split('\n'):
270
+ document.add_paragraph(para)
271
+ doc_buffer = io.BytesIO()
272
+ document.save(doc_buffer)
273
+ doc_buffer.seek(0)
274
+ return doc_buffer
275
+
276
+ def render():
277
+ with st.expander("👤 Personal Information", expanded=True):
278
+ col1, col2 = st.columns(2)
279
+ name = col1.text_input("Full Name", placeholder="Jane Doe")
280
+ profile_pic_file = col1.file_uploader("Upload Profile Picture (optional)", type=['png', 'jpg', 'jpeg'])
281
+
282
+ email = col2.text_input("Email", placeholder="jane@example.com")
283
+ phone = col1.text_input("Phone", placeholder="+1 555 010 0100")
284
+ linkedin = col2.text_input("LinkedIn Profile URL", placeholder="linkedin.com/in/jane-doe")
285
+ summary = st.text_area("Summary / Objective", placeholder="Software engineer with 5 years of experience building…")
286
+ file_stem = sanitize_filename(name or "resume")
287
+ personal_info = {'name': name, 'email': email, 'phone': phone, 'linkedin': linkedin, 'summary': summary}
288
+
289
+ with st.expander("💼 Work Experience"):
290
+ if 'experiences' not in st.session_state:
291
+ st.session_state.experiences = [{'title': '', 'company': '', 'dates': '', 'desc': ''}]
292
+ for i, exp in enumerate(st.session_state.experiences):
293
+ st.divider()
294
+ exp['title'] = st.text_input("Job Title", exp['title'], key=f"exp_title_{i}")
295
+ exp['company'] = st.text_input("Company", exp['company'], key=f"exp_comp_{i}")
296
+ exp['dates'] = st.text_input("Dates (e.g., Jan 2020 - Present)", exp['dates'], key=f"exp_dates_{i}")
297
+ exp['desc'] = st.text_area("Description", exp['desc'], key=f"exp_desc_{i}")
298
+ if st.button("Add Another Experience", key="add_exp"):
299
+ st.session_state.experiences.append({'title': '', 'company': '', 'dates': '', 'desc': ''})
300
+ st.rerun()
301
+
302
+ with st.expander("🎓 Education"):
303
+ if 'educations' not in st.session_state:
304
+ st.session_state.educations = [{'degree': '', 'school': '', 'dates': ''}]
305
+ for i, edu in enumerate(st.session_state.educations):
306
+ st.divider()
307
+ edu['degree'] = st.text_input("Degree", edu['degree'], key=f"edu_degree_{i}")
308
+ edu['school'] = st.text_input("School / University", edu['school'], key=f"edu_school_{i}")
309
+ edu['dates'] = st.text_input("Dates (e.g., 2016 - 2020)", edu['dates'], key=f"edu_dates_{i}")
310
+ if st.button("Add Another Education", key="add_edu"):
311
+ st.session_state.educations.append({'degree': '', 'school': '', 'dates': ''})
312
+ st.rerun()
313
+
314
+ with st.expander("🛠️ Skills"):
315
+ skills = st.text_area("List your skills (comma-separated)", placeholder="Python, SQL, Docker")
316
+
317
+ # --- Cover Letter Section ---
318
+ with st.expander("✉️ Auto Cover Letter Generator"):
319
+ cl_job_title = st.text_input("Job Title for Cover Letter", key="cl_job_title")
320
+ cl_company_name = st.text_input("Company Name for Cover Letter", key="cl_company_name")
321
+ st.text_area("Paste Job Description Here (for your reference)", key="cl_job_desc", help="The text from the job description is not yet used to customize the letter, but is a planned feature.")
322
+
323
+ if st.button("Generate Cover Letter", key="gen_cl", width="stretch"):
324
+ if not all([cl_job_title, cl_company_name, name]):
325
+ st.warning("Please fill in your Name (in Personal Info), the Job Title, and Company Name to generate a cover letter.")
326
+ else:
327
+ with st.spinner("Generating cover letter..."):
328
+ cover_letter_text = generate_cover_letter_text(
329
+ personal_info, skills, cl_job_title, cl_company_name
330
+ )
331
+ st.session_state.cover_letter_text = cover_letter_text
332
+ st.session_state.cover_letter_docx = generate_cover_letter_docx(cover_letter_text).getvalue()
333
+ st.success("Cover letter generated!")
334
+
335
+ if 'cover_letter_text' in st.session_state:
336
+ st.subheader("Generated Cover Letter Preview")
337
+ st.text_area("Preview", st.session_state.cover_letter_text, height=400, key="cl_preview", disabled=True)
338
+
339
+ cl_col1, cl_col2 = st.columns(2)
340
+
341
+ # TXT Download
342
+ cl_col1.download_button(
343
+ label="Download as TXT",
344
+ data=st.session_state.cover_letter_text.encode('utf-8'),
345
+ file_name=f"{file_stem}_Cover_Letter.txt",
346
+ mime="text/plain",
347
+ width="stretch"
348
+ )
349
+
350
+ # DOCX Download
351
+ cl_col2.download_button(
352
+ label="Download as DOCX",
353
+ data=st.session_state.cover_letter_docx,
354
+ file_name=f"{file_stem}_Cover_Letter.docx",
355
+ mime=DOCX_MIME,
356
+ width="stretch"
357
+ )
358
+
359
+ st.divider()
360
+
361
+ template = st.selectbox("Select Resume Template", ("Classic", "Modern"), help="Choose a layout for your generated PDF resume.")
362
+
363
+ if st.button(f"Generate {template} Resume", width="stretch", type="primary"):
364
+ valid_experiences = [exp for exp in st.session_state.experiences if exp['title']]
365
+ valid_educations = [edu for edu in st.session_state.educations if edu['degree']]
366
+ if not name:
367
+ st.warning("Please enter your name to generate the resume.")
368
+ else:
369
+ profile_pic_data = None
370
+ if profile_pic_file:
371
+ profile_pic_data = io.BytesIO(profile_pic_file.getvalue())
372
+
373
+ with st.spinner("Generating documents..."):
374
+ st.session_state.docx_file = generate_docx_resume(personal_info, valid_experiences, valid_educations, skills, profile_pic=profile_pic_data).getvalue()
375
+ # Dispatch to the correct PDF generator based on template selection
376
+ if template == "Classic":
377
+ st.session_state.pdf_file = generate_pdf_resume_classic(personal_info, valid_experiences, valid_educations, skills, profile_pic=profile_pic_data).getvalue()
378
+ else: # Modern
379
+ st.session_state.pdf_file = generate_pdf_resume_modern(personal_info, valid_experiences, valid_educations, skills).getvalue()
380
+ st.success("Resume generated successfully!")
381
+
382
+ if 'docx_file' in st.session_state and 'pdf_file' in st.session_state:
383
+ col1, col2 = st.columns(2)
384
+ col1.download_button("Download as DOCX", st.session_state.docx_file, f"{file_stem}_Resume.docx", DOCX_MIME, width="stretch")
385
+ col2.download_button("Download as PDF", st.session_state.pdf_file, f"{file_stem}_Resume.pdf", "application/pdf", width="stretch")
@@ -0,0 +1,64 @@
1
+ from urllib.parse import urljoin
2
+
3
+ import streamlit as st
4
+ from bs4 import BeautifulSoup
5
+
6
+ from core.net import fetch
7
+
8
+
9
+ def _meta(soup, **attrs):
10
+ tag = soup.find("meta", attrs=attrs)
11
+ return (tag.get("content") or "").strip() if tag else ""
12
+
13
+
14
+ def audit(url: str) -> tuple:
15
+ """Return (findings, details). Each finding is (passed, label, message)."""
16
+ response = fetch(url, timeout=15)
17
+ soup = BeautifulSoup(response.text, "html.parser")
18
+ title = soup.title.get_text(strip=True) if soup.title else ""
19
+ description = _meta(soup, name="description")
20
+ h1s = [h.get_text(" ", strip=True) for h in soup.find_all("h1")]
21
+ images = soup.find_all("img")
22
+ missing_alt = [img for img in images if not (img.get("alt") or "").strip()]
23
+ canonical = soup.find("link", rel="canonical")
24
+ html_tag = soup.find("html")
25
+
26
+ findings = [
27
+ (response.status_code == 200, "HTTP status", f"Returned {response.status_code}."),
28
+ (response.url.startswith("https://"), "HTTPS", "Served over HTTPS." if response.url.startswith("https://") else "Not served over HTTPS."),
29
+ (10 <= len(title) <= 60, "Title", f"{len(title)} characters (aim for 10–60)." if title else "Missing <title>."),
30
+ (50 <= len(description) <= 160, "Meta description", f"{len(description)} characters (aim for 50–160)." if description else "Missing meta description."),
31
+ (len(h1s) == 1, "H1 heading", f"{len(h1s)} found (aim for exactly one)."),
32
+ (not missing_alt, "Image alt text", f"{len(missing_alt)} of {len(images)} images missing alt text."),
33
+ (bool(_meta(soup, name="viewport")), "Mobile viewport", "Viewport meta tag present." if _meta(soup, name="viewport") else "No viewport meta tag."),
34
+ (canonical is not None, "Canonical URL", "Canonical link present." if canonical else "No canonical link."),
35
+ (bool(html_tag and html_tag.get("lang")), "Language", f"lang=\"{html_tag.get('lang')}\"" if html_tag and html_tag.get("lang") else "No lang attribute on <html>."),
36
+ (bool(_meta(soup, property="og:title")), "Open Graph", "og:title present." if _meta(soup, property="og:title") else "No Open Graph tags for social sharing."),
37
+ ]
38
+ details = {
39
+ "Title": title or "—",
40
+ "Meta description": description or "—",
41
+ "H1 headings": h1s or ["—"],
42
+ "Canonical": urljoin(response.url, canonical.get("href", "")) if canonical else "—",
43
+ "Page size": f"{len(response.content) / 1024:.1f} KB",
44
+ }
45
+ return findings, details
46
+
47
+
48
+ def render() -> None:
49
+ url = st.text_input("Page URL", placeholder="https://example.com")
50
+ if not st.button("Run audit", type="primary", disabled=not url.strip()):
51
+ return
52
+ with st.spinner("Auditing…"):
53
+ try:
54
+ findings, details = audit(url)
55
+ except Exception as exc:
56
+ st.error(f"Could not audit page: {exc}")
57
+ return
58
+
59
+ passed = sum(ok for ok, _, _ in findings)
60
+ st.metric("Score", f"{round(passed / len(findings) * 100)} / 100", f"{passed} of {len(findings)} checks passed", delta_color="off")
61
+ for ok, label, message in findings:
62
+ st.markdown(f"{'✅' if ok else '⚠️'} **{label}** — {message}")
63
+ with st.expander("Extracted details"):
64
+ st.json(details)
@@ -0,0 +1,72 @@
1
+ import base64
2
+ import html
3
+
4
+ import streamlit as st
5
+
6
+ TEMPLATES = ["Classic", "Modern", "Minimal"]
7
+
8
+
9
+ def generate_signature_html(name, title, company, phone, email, website, template="Classic", logo_base64=None, logo_mime="image/png"):
10
+ """Return email-client-friendly HTML (inline styles, tables) for a signature."""
11
+ if website and not website.startswith(("http://", "https://")):
12
+ website = "https://" + website
13
+ name, title, company, phone, email, website = map(html.escape, (name, title, company, phone, email, website))
14
+ role = " · ".join(part for part in (title, company) if part)
15
+ logo = (
16
+ f'<img src="data:{logo_mime};base64,{logo_base64}" alt="{company or "Logo"}" '
17
+ f'style="max-width:120px;height:auto;margin-bottom:10px;display:block;">' if logo_base64 else ""
18
+ )
19
+ email_link = f'<a href="mailto:{email}" style="color:#2563EB;text-decoration:none;">{email}</a>' if email else ""
20
+ web_link = f'<a href="{website}" style="color:#2563EB;text-decoration:none;">{website.split("://", 1)[-1]}</a>' if website else ""
21
+
22
+ if template == "Modern":
23
+ contact = "".join(
24
+ f'<p style="margin:2px 0;"><strong>{label}:</strong> {value}</p>'
25
+ for label, value in (("Phone", phone), ("Email", email_link), ("Web", web_link)) if value
26
+ )
27
+ return f"""<table cellpadding="0" cellspacing="0" style="font-family:Helvetica,Arial,sans-serif;color:#374151;border-collapse:collapse;">
28
+ <tr>
29
+ <td style="padding-right:16px;border-right:2px solid #2563EB;vertical-align:top;">
30
+ {logo}<p style="margin:0;font-size:18px;font-weight:bold;color:#111827;">{name}</p>
31
+ <p style="margin:4px 0 0;font-size:14px;color:#6B7280;">{role}</p>
32
+ </td>
33
+ <td style="padding-left:16px;font-size:13px;line-height:1.6;vertical-align:top;">{contact}</td>
34
+ </tr>
35
+ </table>"""
36
+
37
+ if template == "Minimal":
38
+ details = " | ".join(v for v in (email_link, web_link, phone) if v)
39
+ return f"""<div style="font-family:'Courier New',Courier,monospace;font-size:12px;line-height:1.6;color:#4B5563;">
40
+ {logo}<strong>{name}</strong>{f" // {role}" if role else ""}<br>{details}
41
+ </div>"""
42
+
43
+ contact = " | ".join(v for v in (f"<strong>P:</strong> {phone}" if phone else "", f"<strong>E:</strong> {email_link}" if email else "") if v)
44
+ return f"""<div style="font-family:Arial,sans-serif;line-height:1.5;color:#333333;font-size:14px;border-top:1px solid #E5E7EB;padding-top:10px;">
45
+ {logo}<p style="margin:0;"><strong style="font-size:16px;color:#2563EB;">{name}</strong></p>
46
+ <p style="margin:0;color:#6B7280;">{role}</p>
47
+ <p style="margin:8px 0 0;color:#6B7280;">{contact}</p>
48
+ {f'<p style="margin:0;color:#6B7280;"><strong>W:</strong> {web_link}</p>' if website else ""}
49
+ </div>"""
50
+
51
+
52
+ def render() -> None:
53
+ template = st.radio("Template", TEMPLATES, horizontal=True)
54
+ left, right = st.columns(2)
55
+ name = left.text_input("Full name", "Jane Doe")
56
+ title = left.text_input("Job title", "Product Designer")
57
+ company = left.text_input("Company", "Acme Inc.")
58
+ phone = right.text_input("Phone", "+1 555 010 0100")
59
+ email = right.text_input("Email", "jane@acme.com")
60
+ website = right.text_input("Website", "acme.com")
61
+ logo_file = st.file_uploader("Logo (optional)", type=["png", "jpg", "jpeg"])
62
+
63
+ logo_b64 = base64.b64encode(logo_file.getvalue()).decode() if logo_file else None
64
+ logo_mime = logo_file.type if logo_file else "image/png"
65
+ signature = generate_signature_html(name, title, company, phone, email, website, template, logo_b64, logo_mime)
66
+
67
+ light, dark, code = st.tabs(["Light preview", "Dark preview", "HTML"])
68
+ light.markdown(f"<div class='signature-preview light'>{signature}</div>", unsafe_allow_html=True)
69
+ dark.markdown(f"<div class='signature-preview dark'>{signature}</div>", unsafe_allow_html=True)
70
+ with code:
71
+ st.code(signature, language="html")
72
+ st.download_button("Download .html", signature, "email_signature.html", "text/html")
@@ -0,0 +1,68 @@
1
+ import socket
2
+ import ssl
3
+ from datetime import datetime, timezone
4
+ from urllib.parse import urlparse
5
+
6
+ import streamlit as st
7
+
8
+ from core.net import ensure_public_url
9
+
10
+
11
+ def inspect_certificate(url: str) -> dict:
12
+ url = ensure_public_url(url)
13
+ parsed = urlparse(url)
14
+ host, port = parsed.hostname, parsed.port or 443
15
+ context = ssl.create_default_context()
16
+ with socket.create_connection((host, port), timeout=10) as sock:
17
+ with context.wrap_socket(sock, server_hostname=host) as tls:
18
+ cert = tls.getpeercert()
19
+ version = tls.version()
20
+
21
+ def name(field):
22
+ return dict(item[0] for item in cert.get(field, ())).get("commonName") or dict(
23
+ item[0] for item in cert.get(field, ())).get("organizationName", "—")
24
+
25
+ expires = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
26
+ issued = datetime.strptime(cert["notBefore"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
27
+ return {
28
+ "host": host,
29
+ "subject": name("subject"),
30
+ "issuer": name("issuer"),
31
+ "issued": issued,
32
+ "expires": expires,
33
+ "days_left": (expires - datetime.now(timezone.utc)).days,
34
+ "protocol": version,
35
+ "sans": [value for kind, value in cert.get("subjectAltName", ()) if kind == "DNS"],
36
+ }
37
+
38
+
39
+ def render() -> None:
40
+ url = st.text_input("Domain or URL", placeholder="example.com")
41
+ if not st.button("Check certificate", type="primary", disabled=not url.strip()):
42
+ return
43
+ with st.spinner("Connecting…"):
44
+ try:
45
+ info = inspect_certificate(url)
46
+ except ssl.SSLCertVerificationError as exc:
47
+ st.error(f"Certificate is **not valid**: {exc.verify_message}")
48
+ return
49
+ except Exception as exc:
50
+ st.error(f"Could not check certificate: {exc}")
51
+ return
52
+
53
+ days = info["days_left"]
54
+ if days < 0:
55
+ st.error(f"Certificate expired {-days} days ago.")
56
+ elif days < 14:
57
+ st.warning(f"Valid, but expires in {days} days.")
58
+ else:
59
+ st.success(f"Valid certificate — {days} days remaining.")
60
+
61
+ cols = st.columns(3)
62
+ cols[0].metric("Expires", info["expires"].strftime("%d %b %Y"))
63
+ cols[1].metric("Issuer", info["issuer"])
64
+ cols[2].metric("Protocol", info["protocol"])
65
+ st.markdown(f"**Subject:** {info['subject']} \n**Issued:** {info['issued']:%d %b %Y}")
66
+ if info["sans"]:
67
+ with st.expander(f"Covered domains ({len(info['sans'])})"):
68
+ st.write(", ".join(info["sans"]))
@@ -0,0 +1,34 @@
1
+ import io
2
+
3
+ import streamlit as st
4
+
5
+ from core.ui import missing_dependency
6
+
7
+ LANGUAGES = {
8
+ "English": "en", "Bengali": "bn", "Hindi": "hi", "Spanish": "es", "French": "fr",
9
+ "German": "de", "Arabic": "ar", "Chinese (Mandarin)": "zh-CN", "Japanese": "ja", "Portuguese": "pt",
10
+ }
11
+
12
+
13
+ def render() -> None:
14
+ try:
15
+ from gtts import gTTS
16
+ except ImportError:
17
+ missing_dependency("gTTS")
18
+ return
19
+
20
+ text = st.text_area("Text", height=180, placeholder="Type the text you want spoken…", max_chars=5000)
21
+ left, right = st.columns(2)
22
+ language = left.selectbox("Language", list(LANGUAGES))
23
+ slow = right.checkbox("Slow speech")
24
+
25
+ if st.button("Generate audio", type="primary", disabled=not text.strip()):
26
+ with st.spinner("Generating audio…"):
27
+ try:
28
+ buf = io.BytesIO()
29
+ gTTS(text=text, lang=LANGUAGES[language], slow=slow).write_to_fp(buf)
30
+ except Exception as exc:
31
+ st.error(f"Could not generate audio: {exc}")
32
+ return
33
+ st.audio(buf.getvalue(), format="audio/mp3")
34
+ st.download_button("Download MP3", buf.getvalue(), "speech.mp3", "audio/mpeg")
@@ -0,0 +1,36 @@
1
+ import re
2
+
3
+ import streamlit as st
4
+
5
+ TRANSFORMS = {
6
+ "None": lambda t: t,
7
+ "UPPERCASE": str.upper,
8
+ "lowercase": str.lower,
9
+ "Title Case": str.title,
10
+ "Sentence case": lambda t: re.sub(r"(^\s*|[.!?]\s+)([a-z])", lambda m: m.group(1) + m.group(2).upper(), t.lower()),
11
+ "snake_case": lambda t: re.sub(r"\W+", "_", t.strip()).strip("_").lower(),
12
+ "kebab-case": lambda t: re.sub(r"\W+", "-", t.strip()).strip("-").lower(),
13
+ "Remove extra spaces": lambda t: re.sub(r"[ \t]+", " ", re.sub(r"\n{3,}", "\n\n", t)).strip(),
14
+ "Remove line breaks": lambda t: " ".join(t.split()),
15
+ }
16
+
17
+
18
+ def stats(text: str) -> dict:
19
+ words = re.findall(r"\b\w+\b", text)
20
+ return {
21
+ "Characters": len(text),
22
+ "Words": len(words),
23
+ "Sentences": len(re.findall(r"[^.!?]+[.!?]", text)) or (1 if text.strip() else 0),
24
+ "Reading time": f"{max(1, round(len(words) / 200))} min" if words else "0 min",
25
+ }
26
+
27
+
28
+ def render() -> None:
29
+ text = st.text_area("Your text", height=200, placeholder="Paste or type text here…")
30
+ for col, (label, value) in zip(st.columns(4), stats(text).items()):
31
+ col.metric(label, value)
32
+
33
+ transform = st.selectbox("Transform", list(TRANSFORMS))
34
+ if text and transform != "None":
35
+ st.caption("Result — use the copy icon in the top-right corner.")
36
+ st.code(TRANSFORMS[transform](text), language=None, wrap_lines=True)