fh-matui 0.9.4__py3-none-any.whl → 0.9.6__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.
fh_matui/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.4"
1
+ __version__ = "0.9.5"
fh_matui/_modidx.py CHANGED
@@ -128,10 +128,15 @@ d = { 'settings': { 'branch': 'master',
128
128
  'fh_matui.core._ThemeNamespace.violet': ('core.html#_themenamespace.violet', 'fh_matui/core.py'),
129
129
  'fh_matui.core._ThemeNamespace.yellow': ('core.html#_themenamespace.yellow', 'fh_matui/core.py'),
130
130
  'fh_matui.core._ThemeNamespace.zinc': ('core.html#_themenamespace.zinc', 'fh_matui/core.py')},
131
- 'fh_matui.datatable': { 'fh_matui.datatable.DataTable': ('datatable.html#datatable', 'fh_matui/datatable.py'),
131
+ 'fh_matui.datatable': { 'fh_matui.datatable.CrudContext': ('datatable.html#crudcontext', 'fh_matui/datatable.py'),
132
+ 'fh_matui.datatable.DataTable': ('datatable.html#datatable', 'fh_matui/datatable.py'),
132
133
  'fh_matui.datatable.DataTableResource': ('datatable.html#datatableresource', 'fh_matui/datatable.py'),
133
134
  'fh_matui.datatable.DataTableResource.__init__': ( 'datatable.html#datatableresource.__init__',
134
135
  'fh_matui/datatable.py'),
136
+ 'fh_matui.datatable.DataTableResource._build_context': ( 'datatable.html#datatableresource._build_context',
137
+ 'fh_matui/datatable.py'),
138
+ 'fh_matui.datatable.DataTableResource._call_hook': ( 'datatable.html#datatableresource._call_hook',
139
+ 'fh_matui/datatable.py'),
135
140
  'fh_matui.datatable.DataTableResource._error_toast': ( 'datatable.html#datatableresource._error_toast',
136
141
  'fh_matui/datatable.py'),
137
142
  'fh_matui.datatable.DataTableResource._filter_by_search': ( 'datatable.html#datatableresource._filter_by_search',
@@ -152,7 +157,10 @@ d = { 'settings': { 'branch': 'master',
152
157
  'fh_matui/datatable.py'),
153
158
  'fh_matui.datatable.DataTableResource._wrap_modal': ( 'datatable.html#datatableresource._wrap_modal',
154
159
  'fh_matui/datatable.py'),
160
+ 'fh_matui.datatable.DataTableResource._wrap_response': ( 'datatable.html#datatableresource._wrap_response',
161
+ 'fh_matui/datatable.py'),
155
162
  'fh_matui.datatable._action_menu': ('datatable.html#_action_menu', 'fh_matui/datatable.py'),
163
+ 'fh_matui.datatable._is_htmx_request': ('datatable.html#_is_htmx_request', 'fh_matui/datatable.py'),
156
164
  'fh_matui.datatable._page_size_select': ('datatable.html#_page_size_select', 'fh_matui/datatable.py'),
157
165
  'fh_matui.datatable._safe_int': ('datatable.html#_safe_int', 'fh_matui/datatable.py'),
158
166
  'fh_matui.datatable._to_dict': ('datatable.html#_to_dict', 'fh_matui/datatable.py'),
fh_matui/datatable.py CHANGED
@@ -3,7 +3,7 @@
3
3
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_datatable.ipynb.
4
4
 
5
5
  # %% auto 0
6
- __all__ = ['PAGE_SIZES', 'table_state_from_request', 'DataTable', 'DataTableResource']
6
+ __all__ = ['PAGE_SIZES', 'logger', 'table_state_from_request', 'DataTable', 'CrudContext', 'DataTableResource']
7
7
 
8
8
  # %% ../nbs/05_datatable.ipynb 2
9
9
  from fastcore.utils import *
@@ -17,7 +17,7 @@ from .core import *
17
17
  from nbdev.showdoc import show_doc
18
18
  from .components import *
19
19
 
20
- # %% ../nbs/05_datatable.ipynb 6
20
+ # %% ../nbs/05_datatable.ipynb 7
21
21
  #| code-fold: true
22
22
  from math import ceil
23
23
  from urllib.parse import urlencode
@@ -288,7 +288,102 @@ def DataTable(
288
288
 
289
289
  return Div(card, Div(id=feedback_id), id=container_id)
290
290
 
291
- # %% ../nbs/05_datatable.ipynb 8
291
+ # %% ../nbs/05_datatable.ipynb 9
292
+ import asyncio
293
+ import logging
294
+ from dataclasses import dataclass
295
+ from typing import Callable, Any, Optional
296
+ from starlette.responses import HTMLResponse
297
+
298
+ logger = logging.getLogger(__name__)
299
+
300
+ @dataclass
301
+ class CrudContext:
302
+ """
303
+ 🎯 Context object passed to enhanced CRUD operation hooks.
304
+
305
+ Provides rich access to request state, user info, database, and record data.
306
+ Perfect for implementing complex business logic and external API integration.
307
+
308
+ ## 📦 Fields
309
+
310
+ - `request`: Full Starlette request object (headers, query params, session, state)
311
+ - `user`: Current user dict from `request.state.user` (if available)
312
+ - `db`: Database instance from `request.state.tenant_db` (if available)
313
+ - `tbl`: Table instance from `request.state.tables[table_name]` (if available)
314
+ - `record`: Form data dict with field values
315
+ - `record_id`: Record ID for update/delete operations (None for create)
316
+
317
+ ## 💡 Usage in Hooks
318
+
319
+ ### Example: Create with External API
320
+ ```python
321
+ async def quiltt_create_connection(ctx: CrudContext) -> dict:
322
+ # Access user info
323
+ user_id = ctx.user['user_id']
324
+
325
+ # Call external API
326
+ api = QuilttClient()
327
+ response = await api.create_connection(
328
+ institution=ctx.record['institution_name'],
329
+ user_id=user_id
330
+ )
331
+
332
+ # Enrich record with API response
333
+ ctx.record['connection_id'] = response['id']
334
+ ctx.record['account_id'] = response['account_id']
335
+ ctx.record['status'] = 'pending'
336
+
337
+ return ctx.record # DataTableResource will insert this
338
+
339
+ DataTableResource(
340
+ ...,
341
+ on_create=quiltt_create_connection # 🆕 Enhanced hook
342
+ )
343
+ ```
344
+
345
+ ### Example: Soft Delete
346
+ ```python
347
+ def soft_delete_budget(ctx: CrudContext) -> None:
348
+ # Access table directly
349
+ ctx.tbl.update({
350
+ 'id': ctx.record_id,
351
+ 'is_deleted': True,
352
+ 'deleted_at': datetime.now().isoformat(),
353
+ 'deleted_by': ctx.user['user_id']
354
+ })
355
+ # No return needed for delete hooks
356
+
357
+ DataTableResource(
358
+ ...,
359
+ on_delete=soft_delete_budget, # 🆕 Custom delete logic
360
+ get_table=lambda req: req.state.tables['budgets']
361
+ )
362
+ ```
363
+
364
+ ### Example: Update with Sync
365
+ ```python
366
+ async def sync_transaction_update(ctx: CrudContext) -> dict:
367
+ # Update external API first
368
+ api = TransactionAPI()
369
+ await api.update_transaction(
370
+ transaction_id=ctx.record_id,
371
+ data=ctx.record
372
+ )
373
+
374
+ # Add sync timestamp
375
+ ctx.record['last_synced'] = datetime.now().isoformat()
376
+ return ctx.record
377
+ ```
378
+ """
379
+ request: Any # Full Starlette request
380
+ user: Optional[dict] = None # request.state.user (if available)
381
+ db: Optional[Any] = None # request.state.tenant_db (if available)
382
+ tbl: Optional[Any] = None # request.state.tables[table_name] (if available)
383
+ record: dict = None # Form data dict
384
+ record_id: Optional[Any] = None # ID for update/delete (None for create)
385
+
386
+ # %% ../nbs/05_datatable.ipynb 12
292
387
  from typing import Callable, Optional, Any, Union
293
388
  from dataclasses import asdict, is_dataclass
294
389
  from datetime import datetime
@@ -308,19 +403,41 @@ def _to_dict(obj: Any) -> dict:
308
403
  return dict(obj)
309
404
 
310
405
 
406
+ def _is_htmx_request(req) -> bool:
407
+ """Check if request is an HTMX partial request."""
408
+ headers = getattr(req, 'headers', {})
409
+ return headers.get('HX-Request') == 'true'
410
+
411
+
311
412
  class DataTableResource:
312
- "High-level resource that auto-registers all routes for a data table."
413
+ """
414
+ 🔧 High-level resource that auto-registers all routes for a data table.
415
+
416
+ **Features:**
417
+ - All callbacks receive `request` for multi-tenant support
418
+ - Custom CRUD hooks with `CrudContext` for rich business logic
419
+ - Async/sync hook support for external API integration
420
+ - Auto-refresh table via HX-Trigger after mutations
421
+ - Layout wrapper for full-page (non-HTMX) responses
422
+
423
+ **Auto-registers 3 routes:**
424
+ - `GET {base_route}` → DataTable list view
425
+ - `GET {base_route}/action` → FormModal for create/edit/view/delete
426
+ - `POST {base_route}/save` → Save handler with hooks
427
+ """
313
428
 
314
429
  def __init__(
315
430
  self,
316
431
  app,
317
432
  base_route: str,
318
433
  columns: list[dict],
319
- get_all: Callable[[], list],
320
- get_by_id: Callable[[Any], Any],
321
- create: Callable[[dict], Any] = None,
322
- update: Callable[[Any, dict], Any] = None,
323
- delete: Callable[[Any], bool] = None,
434
+ # Data callbacks - ALL receive request as first param
435
+ get_all: Callable[[Any], list], # (req) -> list
436
+ get_by_id: Callable[[Any, Any], Any], # (req, id) -> record
437
+ create: Callable[[Any, dict], Any] = None, # (req, data) -> record
438
+ update: Callable[[Any, Any, dict], Any] = None, # (req, id, data) -> record
439
+ delete: Callable[[Any, Any], bool] = None, # (req, id) -> bool
440
+ # Display options
324
441
  title: str = "Records",
325
442
  row_id_field: str = "id",
326
443
  crud_ops: dict = None,
@@ -328,15 +445,12 @@ class DataTableResource:
328
445
  search_placeholder: str = "Search...",
329
446
  create_label: str = "New Record",
330
447
  empty_message: str = "No records found.",
331
- # Lifecycle hooks
332
- on_before_create: Callable[[dict], dict] = None,
333
- on_after_create: Callable[[Any], None] = None,
334
- on_before_update: Callable[[Any, dict], dict] = None,
335
- on_after_update: Callable[[Any], None] = None,
336
- on_before_delete: Callable[[Any], bool] = None,
337
- on_after_delete: Callable[[Any], None] = None,
338
- # Multi-tenant
339
- user_filter: Callable = None,
448
+ # CRUD Hooks (with CrudContext)
449
+ on_create: Callable[[CrudContext], dict] = None,
450
+ on_update: Callable[[CrudContext], dict] = None,
451
+ on_delete: Callable[[CrudContext], None] = None,
452
+ # Layout wrapper for full-page responses
453
+ layout_wrapper: Callable[[Any, Any], Any] = None, # (content, req) -> wrapped
340
454
  # Custom generators
341
455
  id_generator: Callable[[], Any] = None,
342
456
  timestamp_fields: dict = None
@@ -359,23 +473,20 @@ class DataTableResource:
359
473
  # Determine CRUD ops from provided functions
360
474
  if crud_ops is None:
361
475
  self.crud_ops = {
362
- "create": create is not None,
363
- "update": update is not None,
364
- "delete": delete is not None
476
+ "create": create is not None or on_create is not None,
477
+ "update": update is not None or on_update is not None,
478
+ "delete": delete is not None or on_delete is not None
365
479
  }
366
480
  else:
367
481
  self.crud_ops = crud_ops
368
482
 
369
- # Lifecycle hooks
370
- self.on_before_create = on_before_create
371
- self.on_after_create = on_after_create
372
- self.on_before_update = on_before_update
373
- self.on_after_update = on_after_update
374
- self.on_before_delete = on_before_delete
375
- self.on_after_delete = on_after_delete
483
+ # CRUD hooks
484
+ self.on_create_hook = on_create
485
+ self.on_update_hook = on_update
486
+ self.on_delete_hook = on_delete
376
487
 
377
- # Multi-tenant
378
- self.user_filter = user_filter
488
+ # Layout wrapper
489
+ self.layout_wrapper = layout_wrapper
379
490
 
380
491
  # Generators
381
492
  self.id_generator = id_generator
@@ -385,46 +496,74 @@ class DataTableResource:
385
496
  self.container_id = f"crud-table-{base_route.replace('/', '-').strip('-')}"
386
497
  self.feedback_id = f"{self.container_id}-feedback"
387
498
  self.modal_id = f"{self.container_id}-modal"
499
+ self.refresh_trigger = f"{self.container_id}-refresh"
388
500
 
389
501
  # Register routes
390
502
  self._register_routes()
391
503
 
504
+ async def _call_hook(self, hook, ctx: CrudContext):
505
+ """🔄 Call hook function, handling both sync and async."""
506
+ if hook is None:
507
+ return None
508
+ if asyncio.iscoroutinefunction(hook):
509
+ return await hook(ctx)
510
+ return hook(ctx)
511
+
512
+ def _build_context(self, req, record: dict = None, record_id: Any = None) -> CrudContext:
513
+ """🏗️ Build CrudContext from request."""
514
+ user = None
515
+ db = None
516
+ tbl = None
517
+
518
+ # Extract user from request.state if available
519
+ try:
520
+ if hasattr(req, 'state') and hasattr(req.state, 'user'):
521
+ user = req.state.user
522
+ except AttributeError:
523
+ pass
524
+
525
+ # Extract db from request.state if available
526
+ try:
527
+ if hasattr(req, 'state') and hasattr(req.state, 'tenant_db'):
528
+ db = req.state.tenant_db
529
+ except AttributeError:
530
+ pass
531
+
532
+ return CrudContext(
533
+ request=req,
534
+ user=user,
535
+ db=db,
536
+ tbl=tbl,
537
+ record=record or {},
538
+ record_id=record_id
539
+ )
540
+
541
+ def _wrap_response(self, content, req):
542
+ """Wrap content with layout_wrapper if not an HTMX request."""
543
+ if self.layout_wrapper and not _is_htmx_request(req):
544
+ return self.layout_wrapper(content, req)
545
+ return content
546
+
392
547
  def _register_routes(self):
393
548
  """Register all data table routes with the app."""
394
549
  rt = self.app.route
395
550
 
396
- # Main table route
397
551
  @rt(self.base_route)
398
552
  def _table_handler(req):
399
553
  return self._handle_table(req)
400
554
 
401
- # Action route (view/edit/create/delete)
402
555
  @rt(f"{self.base_route}/action")
403
556
  def _action_handler(req):
404
557
  return self._handle_action(req)
405
558
 
406
- # Save route (form submission)
407
559
  @rt(f"{self.base_route}/save")
408
560
  async def _save_handler(req):
409
561
  return await self._handle_save(req)
410
562
 
411
563
  def _get_filtered_data(self, req) -> list:
412
- """Get all data, optionally filtered by user_filter."""
413
- all_data = self.get_all()
414
-
415
- # Convert to list of dicts
416
- data = [_to_dict(item) for item in all_data]
417
-
418
- # Apply user filter if provided
419
- if self.user_filter and callable(self.user_filter):
420
- filter_criteria = self.user_filter(req)
421
- if filter_criteria:
422
- data = [
423
- row for row in data
424
- if all(row.get(k) == v for k, v in filter_criteria.items())
425
- ]
426
-
427
- return data
564
+ """Get all data from user's callback."""
565
+ all_data = self.get_all(req)
566
+ return [_to_dict(item) for item in all_data]
428
567
 
429
568
  def _filter_by_search(self, data: list, search: str) -> list:
430
569
  """Filter data by search term across searchable columns."""
@@ -437,7 +576,6 @@ class DataTableResource:
437
576
  if col.get("searchable", False)
438
577
  ]
439
578
 
440
- # If no columns marked searchable, search all
441
579
  if not searchable_keys:
442
580
  searchable_keys = [col["key"] for col in self.columns]
443
581
 
@@ -451,26 +589,19 @@ class DataTableResource:
451
589
  total = len(data)
452
590
  total_pages = max(1, ceil(total / page_size))
453
591
  page = min(max(1, page), total_pages)
454
-
455
592
  start = (page - 1) * page_size
456
593
  end = start + page_size
457
-
458
594
  return data[start:end], total, page
459
595
 
460
596
  def _handle_table(self, req):
461
597
  """Handle main table route."""
462
- # Extract pagination state
463
598
  state = table_state_from_request(req, page_sizes=self.page_sizes)
464
599
  search, page, page_size = state["search"], state["page"], state["page_size"]
465
600
 
466
- # Get and filter data
467
601
  data = self._get_filtered_data(req)
468
602
  filtered = self._filter_by_search(data, search)
469
-
470
- # Paginate
471
603
  page_data, total, page = self._paginate(filtered, page, page_size)
472
604
 
473
- # Build table
474
605
  table = DataTable(
475
606
  data=page_data,
476
607
  total=total,
@@ -489,31 +620,43 @@ class DataTableResource:
489
620
  empty_message=self.empty_message
490
621
  )
491
622
 
492
- return table
623
+ # Wrap table in auto-refresh container
624
+ params = urlencode({"search": search, "page": page, "page_size": page_size})
625
+ table_container = Div(
626
+ table,
627
+ id=self.container_id,
628
+ hx_trigger=f"{self.refresh_trigger} from:body",
629
+ hx_get=f"{self.base_route}?{params}",
630
+ hx_target=f"#{self.container_id}",
631
+ hx_swap="outerHTML"
632
+ )
633
+
634
+ feedback = Div(id=self.feedback_id)
635
+ content = Div(feedback, table_container)
636
+
637
+ # Wrap with layout if full-page request
638
+ return self._wrap_response(content, req)
493
639
 
494
640
  def _handle_action(self, req):
495
641
  """Handle action route (view/edit/create/delete)."""
496
642
  params = getattr(req, "query_params", {})
497
643
  getter = params.get if hasattr(params, "get") else (lambda k, d=None: params[k] if k in params else d)
498
644
 
499
- # Handle dismiss
500
645
  if getter("dismiss") is not None:
501
646
  return Div(id=self.feedback_id)
502
647
 
503
648
  record_id = getter("id")
504
- # Try to convert to int if numeric
505
649
  if record_id:
506
650
  try:
507
651
  record_id = int(record_id)
508
652
  except (TypeError, ValueError):
509
- pass # Keep as string (e.g., UUID)
653
+ pass
510
654
 
511
655
  action = (getter("action", "view") or "view").lower()
512
656
  search = getter("search", "") or ""
513
657
  page = _safe_int(getter("page", 1), 1)
514
658
  page_size = _safe_int(getter("page_size", 10), 10)
515
659
 
516
- # URLs
517
660
  return_params = urlencode({"search": search, "page": page, "page_size": page_size})
518
661
  cancel_url = f"{self.base_route}/action?dismiss=1"
519
662
  save_url = f"{self.base_route}/save?{return_params}"
@@ -539,7 +682,7 @@ class DataTableResource:
539
682
  # Get record for view/edit/delete
540
683
  record = None
541
684
  if record_id:
542
- raw_record = self.get_by_id(record_id)
685
+ raw_record = self.get_by_id(req, record_id)
543
686
  record = _to_dict(raw_record) if raw_record else None
544
687
 
545
688
  if not record:
@@ -581,21 +724,23 @@ class DataTableResource:
581
724
  if not self.crud_ops.get("delete"):
582
725
  return self._error_toast("Delete operation not enabled.")
583
726
 
584
- # Check before_delete hook
585
- if self.on_before_delete:
586
- if not self.on_before_delete(record_id):
587
- return self._error_toast("Delete cancelled by validation.")
588
-
589
- # Perform delete
590
727
  try:
591
- self.delete_fn(record_id)
728
+ ctx = self._build_context(req, record=record, record_id=record_id)
592
729
 
593
- # After delete hook
594
- if self.on_after_delete:
595
- self.on_after_delete(record_id)
730
+ # Use on_delete hook if provided
731
+ if self.on_delete_hook:
732
+ if asyncio.iscoroutinefunction(self.on_delete_hook):
733
+ loop = asyncio.get_event_loop()
734
+ loop.run_until_complete(self.on_delete_hook(ctx))
735
+ else:
736
+ self.on_delete_hook(ctx)
737
+ else:
738
+ # Default: use delete_fn
739
+ self.delete_fn(req, record_id)
596
740
 
597
- return self._success_toast(f"Record deleted successfully.")
741
+ return self._success_toast("Record deleted successfully.")
598
742
  except Exception as e:
743
+ logger.error(f"Delete failed: {e}", exc_info=True)
599
744
  return self._error_toast(f"Delete failed: {str(e)}")
600
745
 
601
746
  return Div(id=self.feedback_id)
@@ -606,14 +751,13 @@ class DataTableResource:
606
751
  form_data = await req.form()
607
752
  record_id = form_data.get(self.row_id_field)
608
753
 
609
- # Try to convert ID
610
754
  if record_id:
611
755
  try:
612
756
  record_id = int(record_id)
613
757
  except (TypeError, ValueError):
614
758
  pass
615
759
 
616
- # Build data dict from form, excluding the ID field
760
+ # Build data dict from form
617
761
  data = {}
618
762
  for col in self.columns:
619
763
  key = col["key"]
@@ -626,7 +770,6 @@ class DataTableResource:
626
770
 
627
771
  value = form_data.get(key)
628
772
 
629
- # Type conversion based on form config
630
773
  field_type = form_cfg.get("type", "text")
631
774
  if field_type == "number" and value:
632
775
  try:
@@ -648,33 +791,36 @@ class DataTableResource:
648
791
 
649
792
  # CREATE or UPDATE
650
793
  if record_id:
651
- # UPDATE
652
- if self.on_before_update:
653
- data = self.on_before_update(record_id, data)
654
-
655
- result = self.update_fn(record_id, data)
794
+ # ===== UPDATE =====
795
+ ctx = self._build_context(req, record=data, record_id=record_id)
656
796
 
657
- if self.on_after_update:
658
- self.on_after_update(result)
797
+ if self.on_update_hook:
798
+ data = await self._call_hook(self.on_update_hook, ctx)
799
+ if data is not None:
800
+ self.update_fn(req, record_id, data)
801
+ else:
802
+ self.update_fn(req, record_id, data)
659
803
 
660
804
  return self._success_toast("Record updated successfully.")
661
805
  else:
662
- # CREATE
663
- if self.on_before_create:
664
- data = self.on_before_create(data)
665
-
806
+ # ===== CREATE =====
666
807
  # Generate ID if needed
667
- if self.id_generator:
808
+ if self.id_generator and self.row_id_field not in data:
668
809
  data[self.row_id_field] = self.id_generator()
669
810
 
670
- result = self.create_fn(data)
811
+ ctx = self._build_context(req, record=data, record_id=None)
671
812
 
672
- if self.on_after_create:
673
- self.on_after_create(result)
813
+ if self.on_create_hook:
814
+ data = await self._call_hook(self.on_create_hook, ctx)
815
+ if data is not None:
816
+ self.create_fn(req, data)
817
+ else:
818
+ self.create_fn(req, data)
674
819
 
675
820
  return self._success_toast("Record created successfully.")
676
821
 
677
822
  except Exception as e:
823
+ logger.error(f"Save failed: {e}", exc_info=True)
678
824
  return self._error_toast(f"Save failed: {str(e)}")
679
825
 
680
826
  def _wrap_modal(self, modal):
@@ -684,7 +830,7 @@ class DataTableResource:
684
830
  return Div(modal, id=self.feedback_id)
685
831
 
686
832
  def _success_toast(self, message: str):
687
- """Return a success toast in the feedback div."""
833
+ """Return a success toast with auto-refresh trigger."""
688
834
  toast = Toast(
689
835
  message,
690
836
  variant="success",
@@ -698,10 +844,16 @@ class DataTableResource:
698
844
  ),
699
845
  active=True
700
846
  )
701
- return Div(toast, id=self.feedback_id)
847
+
848
+ # Return as HTMLResponse with HX-Trigger for auto-refresh
849
+ from fasthtml.common import to_xml
850
+ html_content = to_xml(Div(toast, id=self.feedback_id))
851
+ response = HTMLResponse(content=html_content)
852
+ response.headers['HX-Trigger'] = self.refresh_trigger
853
+ return response
702
854
 
703
855
  def _error_toast(self, message: str):
704
- """Return an error toast in the feedback div."""
856
+ """Return an error toast (no auto-refresh on errors)."""
705
857
  toast = Toast(
706
858
  message,
707
859
  variant="error",
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: fh-matui
3
+ Version: 0.9.6
4
+ Summary: material-ui for fasthtml
5
+ Home-page: https://github.com/abhisheksreesaila/fh-matui
6
+ Author: abhishek sreesaila
7
+ Author-email: abhishek.sreesaila@gmail.com
8
+ License: Apache Software License 2.0
9
+ Keywords: nbdev jupyter notebook python
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: python-fasthtml
22
+ Requires-Dist: fastcore
23
+ Requires-Dist: markdown
24
+ Provides-Extra: dev
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: home-page
31
+ Dynamic: keywords
32
+ Dynamic: license
33
+ Dynamic: license-file
34
+ Dynamic: provides-extra
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # fh-matui
40
+
41
+
42
+ <!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
43
+
44
+ ## What is fh-matui?
45
+
46
+ **fh-matui** is a Python library that brings Google’s Material Design to
47
+ [FastHTML](https://fastht.ml/) applications. It provides a comprehensive
48
+ set of pre-built UI components that integrate seamlessly with FastHTML’s
49
+ hypermedia-driven architecture.
50
+
51
+ Built on top of [BeerCSS](https://www.beercss.com/) (a lightweight
52
+ Material Design 3 CSS framework), fh-matui enables you to create modern,
53
+ responsive web interfaces entirely in Python — no JavaScript required.
54
+
55
+ ## ✨ Key Features
56
+
57
+ | Feature | Description |
58
+ |----|----|
59
+ | 🎨 **Material Design 3** | Modern, beautiful components following Google’s latest design language |
60
+ | ⚡ **Zero JavaScript** | Build interactive UIs entirely in Python with FastHTML |
61
+ | 📱 **Responsive** | Mobile-first design with automatic breakpoint handling |
62
+ | 🌙 **Dark Mode** | Built-in light/dark theme support with 20+ color schemes |
63
+ | 🧩 **Composable** | Chainable styling APIs inspired by MonsterUI |
64
+ | 📊 **Data Tables** | Full-featured tables with pagination, search, sorting, and CRUD |
65
+ | 🔧 **nbdev-powered** | Literate programming with documentation built from notebooks |
66
+
67
+ ## 🏗️ Architecture
68
+
69
+ ┌─────────────────────────────────────────────────────────────┐
70
+ │ fh-matui │
71
+ ├─────────────────────────────────────────────────────────────┤
72
+ │ Foundations │ Core styling utilities, helpers, enums │
73
+ │ Core │ Theme system, MatTheme color presets │
74
+ │ Components │ Buttons, Cards, Modals, Forms, Tables │
75
+ │ App Pages │ Full-page layouts, navigation patterns │
76
+ │ Data Tables │ DataTable, DataTableResource for CRUD │
77
+ │ Web Pages │ Landing pages, marketing components │
78
+ ├─────────────────────────────────────────────────────────────┤
79
+ │ BeerCSS │ Material Design 3 CSS framework │
80
+ │ FastHTML │ Python hypermedia web framework │
81
+ └─────────────────────────────────────────────────────────────┘
82
+
83
+ ## 🎨 Available Themes
84
+
85
+ fh-matui includes 15+ pre-configured Material Design 3 color themes:
86
+
87
+ | Theme | Preview | Theme | Preview |
88
+ |-----------------------|---------|-----------------------|---------|
89
+ | `MatTheme.red` | 🔴 | `MatTheme.pink` | 🩷 |
90
+ | `MatTheme.purple` | 🟣 | `MatTheme.deepPurple` | 💜 |
91
+ | `MatTheme.indigo` | 🔵 | `MatTheme.blue` | 💙 |
92
+ | `MatTheme.lightBlue` | 🩵 | `MatTheme.cyan` | 🌊 |
93
+ | `MatTheme.teal` | 🩶 | `MatTheme.green` | 💚 |
94
+ | `MatTheme.lightGreen` | 🍀 | `MatTheme.lime` | 💛 |
95
+ | `MatTheme.yellow` | 🌟 | `MatTheme.amber` | 🧡 |
96
+ | `MatTheme.orange` | 🟠 | `MatTheme.deepOrange` | 🔶 |
97
+
98
+ **Usage:**
99
+
100
+ ``` python
101
+ # Choose your theme
102
+ app, rt = fast_app(hdrs=[MatTheme.deepPurple.headers()])
103
+ ```
104
+
105
+ ## 🚀 Quick Start
106
+
107
+ Here’s a minimal example to get you started:
108
+
109
+ ``` python
110
+ from fasthtml.common import *
111
+ from fh_matui.core import MatTheme
112
+ from fh_matui.components import Button, Card, FormField
113
+
114
+ # Create a themed FastHTML app
115
+ app, rt = fast_app(hdrs=[MatTheme.indigo.headers()])
116
+
117
+ @rt('/')
118
+ def home():
119
+ return Div(
120
+ Card(
121
+ H3("Welcome to fh-matui!"),
122
+ P("Build beautiful Material Design apps with Python."),
123
+ FormField("email", label="Email", type="email"),
124
+ Button("Get Started", cls="primary"),
125
+ ),
126
+ cls="padding"
127
+ )
128
+
129
+ serve()
130
+ ```
131
+
132
+ ## 📦 Installation
133
+
134
+ ``` bash
135
+ pip install fh-matui
136
+ ```
137
+
138
+ ### Dependencies
139
+
140
+ fh-matui automatically includes: - **python-fasthtml** - The core
141
+ FastHTML framework - **BeerCSS** - Loaded via CDN for Material Design 3
142
+ styling
143
+
144
+ ### What This Code Does
145
+
146
+ 1. **`MatTheme.indigo.headers()`** - Loads BeerCSS with the indigo
147
+ color scheme
148
+ 2. **[`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card)** -
149
+ Creates a Material Design card component with elevation
150
+ 3. **[`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield)** -
151
+ Generates a styled input with floating label
152
+ 4. **`Button`** - Renders a Material Design button with ripple effects
153
+
154
+ ## 📚 Module Reference
155
+
156
+ | Module | Description | Key Components |
157
+ |----|----|----|
158
+ | [Foundations](foundations.html) | Base utilities and helper functions | `BeerHeaders`, `display`, styling helpers |
159
+ | [Core](core.html) | Theme system and styling | `MatTheme`, color presets, theme configuration |
160
+ | [Components](components.html) | UI component library | `Button`, [`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card), [`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield), [`FormModal`](https://abhisheksreesaila.github.io/fh-matui/components.html#formmodal), [`Grid`](https://abhisheksreesaila.github.io/fh-matui/components.html#grid) |
161
+ | [App Pages](app_pages.html) | Application layouts | Navigation, sidebars, full-page layouts |
162
+ | [Data Tables](05_table.html) | Data management components | [`DataTable`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatable), [`DataTableResource`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatableresource), CRUD operations |
163
+ | [Web Pages](web_pages.html) | Marketing/landing pages | Hero sections, feature grids, testimonials |
164
+
165
+ ## 🛠️ Development
166
+
167
+ ### Install in Development Mode
168
+
169
+ ``` bash
170
+ # Clone the repository
171
+ git clone https://github.com/user/fh-matui.git
172
+ cd fh-matui
173
+
174
+ # Install in editable mode
175
+ pip install -e .
176
+
177
+ # Make changes under nbs/ directory, then compile
178
+ nbdev_prepare
179
+ ```
180
+
181
+ ## 🤝 Why fh-matui?
182
+
183
+ | Challenge | fh-matui Solution |
184
+ |----|----|
185
+ | **CSS complexity** | Pre-built Material Design 3 components via BeerCSS |
186
+ | **JavaScript fatigue** | FastHTML handles interactivity declaratively |
187
+ | **Component consistency** | Unified API across all components |
188
+ | **Dark mode support** | Built-in with automatic system preference detection |
189
+ | **Responsive design** | Mobile-first grid system and responsive utilities |
190
+ | **Form handling** | [`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield), [`FormGrid`](https://abhisheksreesaila.github.io/fh-matui/components.html#formgrid), [`FormModal`](https://abhisheksreesaila.github.io/fh-matui/components.html#formmodal) for rapid form building |
191
+ | **Data management** | [`DataTable`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatable) and [`DataTableResource`](https://abhisheksreesaila.github.io/fh-matui/datatable.html#datatableresource) for CRUD operations |
192
+
193
+ ## 🤖 For LLM Users
194
+
195
+ fh-matui includes **comprehensive documentation bundles** for Large
196
+ Language Models, enabling AI assistants (like Claude, ChatGPT, or GitHub
197
+ Copilot) to help you build FastHTML apps with complete knowledge of the
198
+ component APIs.
199
+
200
+ ### 📥 Download Context File
201
+
202
+ **[📄
203
+ llms-ctx.txt](https://raw.githubusercontent.com/abhisheksreesaila/fh-matui/main/llms-ctx.txt)**
204
+ — Complete API documentation in LLM-optimized format
205
+
206
+ ### 💡 How to Use
207
+
208
+ 1. **Download the context file** from the link above
209
+ 2. **Attach it to your LLM conversation** (drag & drop or paste
210
+ contents)
211
+ 3. **Ask for implementation** using natural language
212
+
213
+ **Example Prompt:**
214
+
215
+ I'm using fh-matui (context attached). Create a dashboard with:
216
+ - A sidebar navigation with 5 menu items
217
+ - A DataTable showing products with pagination
218
+ - A modal form to add/edit products
219
+ - Use the deep purple theme
220
+
221
+ The LLM will generate production-ready FastHTML code using the exact
222
+ component APIs from the documentation.
223
+
224
+ ### 🔄 Staying Up to Date
225
+
226
+ The `llms-ctx.txt` file is automatically regenerated with each release
227
+ to ensure it stays synchronized with the latest API changes. Always
228
+ download the version matching your installed package version for the
229
+ most accurate results.
230
+
231
+ > **📌 Note:** The context file is generated from the same literate
232
+ > programming notebooks that build the library itself, ensuring 100%
233
+ > accuracy with the actual implementation.
234
+
235
+ ## 📄 License
236
+
237
+ This project is licensed under the Apache 2.0 License - see the
238
+ [LICENSE](https://github.com/user/fh-matui/blob/main/LICENSE) file for
239
+ details.
240
+
241
+ ------------------------------------------------------------------------
242
+
243
+ **Built with ❤️ using FastHTML and nbdev**
@@ -0,0 +1,14 @@
1
+ fh_matui/__init__.py,sha256=UvB3IZAWAs7vV6H9dpZvZPryrSNIyF5_bowVS4_YGFA,23
2
+ fh_matui/_modidx.py,sha256=naHwPQ4kCo-5saE_uozmdPA881kp1gnqvKhmaG-Ya-4,23914
3
+ fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
4
+ fh_matui/components.py,sha256=KjdTHzWRXpVWBEIGskW1HfhjPpzRYzi6UA_yRjZyMWM,48254
5
+ fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
6
+ fh_matui/datatable.py,sha256=x5HgBWksvBiJyRE0Ux7Ht7n1hy0AZttTVXU0mMiZ8Vo,31714
7
+ fh_matui/foundations.py,sha256=b7PnObJpKN8ZAU9NzCm9xpfnHzFjjAROU7E2YvA_tj4,1820
8
+ fh_matui/web_pages.py,sha256=4mF-jpfVcZTVepfQ-aMGgIUp-nBp0YCkvcdsWhUYeaA,34879
9
+ fh_matui-0.9.6.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
+ fh_matui-0.9.6.dist-info/METADATA,sha256=-tOPC56fiu90DdvaLQt1C7TOoThZyTJ4m3owELT0BUk,10490
11
+ fh_matui-0.9.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ fh_matui-0.9.6.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
+ fh_matui-0.9.6.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
+ fh_matui-0.9.6.dist-info/RECORD,,
@@ -1,187 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fh-matui
3
- Version: 0.9.4
4
- Summary: material-ui for fasthtml
5
- Home-page: https://github.com/abhisheksreesaila/fh-matui
6
- Author: abhishek sreesaila
7
- Author-email: abhishek.sreesaila@gmail.com
8
- License: Apache Software License 2.0
9
- Keywords: nbdev jupyter notebook python
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Natural Language :: English
13
- Classifier: Programming Language :: Python :: 3.9
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: License :: OSI Approved :: Apache Software License
18
- Requires-Python: >=3.9
19
- Description-Content-Type: text/markdown
20
- License-File: LICENSE
21
- Requires-Dist: python-fasthtml
22
- Requires-Dist: fastcore
23
- Requires-Dist: markdown
24
- Provides-Extra: dev
25
- Dynamic: author
26
- Dynamic: author-email
27
- Dynamic: classifier
28
- Dynamic: description
29
- Dynamic: description-content-type
30
- Dynamic: home-page
31
- Dynamic: keywords
32
- Dynamic: license
33
- Dynamic: license-file
34
- Dynamic: provides-extra
35
- Dynamic: requires-dist
36
- Dynamic: requires-python
37
- Dynamic: summary
38
-
39
-
40
- ## What is fh-matui?
41
-
42
- **fh-matui** is a Python library that brings Google's Material Design to [FastHTML](https://fastht.ml/) applications. It provides a comprehensive set of pre-built UI components that integrate seamlessly with FastHTML's hypermedia-driven architecture.
43
-
44
- Built on top of [BeerCSS](https://www.beercss.com/) (a lightweight Material Design 3 CSS framework), fh-matui enables you to create modern, responsive web interfaces entirely in Python — no JavaScript required.
45
-
46
- ## ✨ Key Features
47
-
48
- | Feature | Description |
49
- |---------|-------------|
50
- | 🎨 **Material Design 3** | Modern, beautiful components following Google's latest design language |
51
- | ⚡ **Zero JavaScript** | Build interactive UIs entirely in Python with FastHTML |
52
- | 📱 **Responsive** | Mobile-first design with automatic breakpoint handling |
53
- | 🌙 **Dark Mode** | Built-in light/dark theme support with 20+ color schemes |
54
- | 🧩 **Composable** | Chainable styling APIs inspired by MonsterUI |
55
- | 📊 **Data Tables** | Full-featured tables with pagination, search, sorting, and CRUD |
56
- | 🔧 **nbdev-powered** | Literate programming with documentation built from notebooks |
57
-
58
- ## 🏗️ Architecture
59
-
60
- ```
61
- ┌─────────────────────────────────────────────────────────────┐
62
- │ fh-matui │
63
- ├─────────────────────────────────────────────────────────────┤
64
- │ Foundations │ Core styling utilities, helpers, enums │
65
- │ Core │ Theme system, MatTheme color presets │
66
- │ Components │ Buttons, Cards, Modals, Forms, Tables │
67
- │ App Pages │ Full-page layouts, navigation patterns │
68
- │ Data Tables │ DataTable, DataTableResource for CRUD │
69
- │ Web Pages │ Landing pages, marketing components │
70
- ├─────────────────────────────────────────────────────────────┤
71
- │ BeerCSS │ Material Design 3 CSS framework │
72
- │ FastHTML │ Python hypermedia web framework │
73
- └─────────────────────────────────────────────────────────────┘
74
- ```
75
-
76
- ## 🎨 Available Themes
77
-
78
- fh-matui includes 15+ pre-configured Material Design 3 color themes:
79
-
80
- | Theme | Preview | Theme | Preview |
81
- |-------|---------|-------|---------|
82
- | `MatTheme.red` | 🔴 | `MatTheme.pink` | 🩷 |
83
- | `MatTheme.purple` | 🟣 | `MatTheme.deepPurple` | 💜 |
84
- | `MatTheme.indigo` | 🔵 | `MatTheme.blue` | 💙 |
85
- | `MatTheme.lightBlue` | 🩵 | `MatTheme.cyan` | 🌊 |
86
- | `MatTheme.teal` | 🩶 | `MatTheme.green` | 💚 |
87
- | `MatTheme.lightGreen` | 🍀 | `MatTheme.lime` | 💛 |
88
- | `MatTheme.yellow` | 🌟 | `MatTheme.amber` | 🧡 |
89
- | `MatTheme.orange` | 🟠 | `MatTheme.deepOrange` | 🔶 |
90
-
91
- **Usage:**
92
- ```python
93
- # Choose your theme
94
- app, rt = fast_app(hdrs=[MatTheme.deepPurple.headers()])
95
- ```
96
-
97
- ## 🚀 Quick Start
98
-
99
- Here's a minimal example to get you started:
100
-
101
- ```python
102
- from fasthtml.common import *
103
- from fh_matui.core import MatTheme
104
- from fh_matui.components import Button, Card, FormField
105
-
106
- # Create a themed FastHTML app
107
- app, rt = fast_app(hdrs=[MatTheme.indigo.headers()])
108
-
109
- @rt('/')
110
- def home():
111
- return Div(
112
- Card(
113
- H3("Welcome to fh-matui!"),
114
- P("Build beautiful Material Design apps with Python."),
115
- Button("Get Started", cls="primary"),
116
- ),
117
- cls="padding"
118
- )
119
-
120
- serve()
121
- ```
122
-
123
- ## 📦 Installation
124
-
125
- ```bash
126
- pip install fh-matui
127
- ```
128
-
129
- ### Dependencies
130
-
131
- fh-matui automatically includes:
132
- - **python-fasthtml** - The core FastHTML framework
133
- - **BeerCSS** - Loaded via CDN for Material Design 3 styling
134
-
135
- ### What This Code Does
136
-
137
- 1. **`MatTheme.indigo.headers()`** - Loads BeerCSS with the indigo color scheme
138
- 2. **[`Card`](https://abhisheksreesaila.github.io/fh-matui/components.html#card)** - Creates a Material Design card component with elevation
139
- 3. **[`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield)** - Generates a styled input with floating label
140
- 4. **`Button`** - Renders a Material Design button with ripple effects
141
-
142
- ## 📚 Module Reference
143
-
144
- | Module | Description | Key Components |
145
- |--------|-------------|----------------|
146
- | [Foundations](https://abhisheksreesaila.github.io/fh-matui/foundations.html) | Base utilities and helper functions | `BeerHeaders`, `display`, styling helpers |
147
- | [Core](https://abhisheksreesaila.github.io/fh-matui/core.html) | Theme system and styling | `MatTheme`, color presets, theme configuration |
148
- | [Components](https://abhisheksreesaila.github.io/fh-matui/components.html) | UI component library | `Button`, `Card`, `FormField`, `FormModal`, `Grid` |
149
- | [App Pages](https://abhisheksreesaila.github.io/fh-matui/app_pages.html) | Application layouts | Navigation, sidebars, full-page layouts |
150
- | [Data Tables](https://abhisheksreesaila.github.io/fh-matui/datatable.html) | Data management components | `DataTable`, `DataTableResource`, CRUD operations |
151
- | [Web Pages](https://abhisheksreesaila.github.io/fh-matui/web_pages.html) | Marketing/landing pages | Hero sections, feature grids, testimonials |
152
-
153
- ## 🛠️ Development
154
-
155
- ### Install in Development Mode
156
-
157
- ```bash
158
- # Clone the repository
159
- git clone https://github.com/user/fh-matui.git
160
- cd fh-matui
161
-
162
- # Install in editable mode
163
- pip install -e .
164
-
165
- # Make changes under nbs/ directory, then compile
166
- nbdev_prepare
167
- ```
168
-
169
- ## 🤝 Why fh-matui?
170
-
171
- | Challenge | fh-matui Solution |
172
- |-----------|-------------------|
173
- | **CSS complexity** | Pre-built Material Design 3 components via BeerCSS |
174
- | **JavaScript fatigue** | FastHTML handles interactivity declaratively |
175
- | **Component consistency** | Unified API across all components |
176
- | **Dark mode support** | Built-in with automatic system preference detection |
177
- | **Responsive design** | Mobile-first grid system and responsive utilities |
178
- | **Form handling** | [`FormField`](https://abhisheksreesaila.github.io/fh-matui/components.html#formfield), [`FormGrid`](https://abhisheksreesaila.github.io/fh-matui/components.html#formgrid), [`FormModal`](https://abhisheksreesaila.github.io/fh-matui/components.html#formmodal) for rapid form building |
179
- | **Data management** | [`DataTable`](https://abhisheksreesaila.github.io/fh-matui/table.html#datatable) and [`DataTableResource`](https://abhisheksreesaila.github.io/fh-matui/table.html#datatableresource) for CRUD operations |
180
-
181
- ## 📄 License
182
-
183
- This project is licensed under the Apache 2.0 License - see the [LICENSE](https://github.com/user/fh-matui/blob/main/LICENSE) file for details.
184
-
185
- ---
186
-
187
- **Built with ❤️ using FastHTML and nbdev**
@@ -1,14 +0,0 @@
1
- fh_matui/__init__.py,sha256=iPcoATf7BiWjSu-KocRdM5zFTR4wx4ktCHlGGpvdc1M,23
2
- fh_matui/_modidx.py,sha256=hLZ_V7aRsAvavJ82m02XyvImfzYB5_DnOce8eKRD3xU,22865
3
- fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
4
- fh_matui/components.py,sha256=KjdTHzWRXpVWBEIGskW1HfhjPpzRYzi6UA_yRjZyMWM,48254
5
- fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
6
- fh_matui/datatable.py,sha256=MAEibyjRwYlBLMD9dIIocufuWn84jTvrgRt1XKFAN9U,25506
7
- fh_matui/foundations.py,sha256=b7PnObJpKN8ZAU9NzCm9xpfnHzFjjAROU7E2YvA_tj4,1820
8
- fh_matui/web_pages.py,sha256=4mF-jpfVcZTVepfQ-aMGgIUp-nBp0YCkvcdsWhUYeaA,34879
9
- fh_matui-0.9.4.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
- fh_matui-0.9.4.dist-info/METADATA,sha256=CBK48Mce-W9Lgll_Pj6XvYGDG_GFkqVor8aCqwI84fk,8369
11
- fh_matui-0.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- fh_matui-0.9.4.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
- fh_matui-0.9.4.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
- fh_matui-0.9.4.dist-info/RECORD,,