ltc-code 0.1.92__tar.gz → 0.1.93__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ltc-code
3
- Version: 0.1.92
3
+ Version: 0.1.93
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ltc-code"
3
- version = "0.1.92"
3
+ version = "0.1.93"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -413,16 +413,19 @@ final = df.pipe(
413
413
 
414
414
 
415
415
 
416
+ import re
416
417
  from typing import Mapping, Optional, Sequence, Union
417
418
 
418
419
  import polars as pl
419
420
 
420
421
 
421
- DEFAULT_PRIORITY_PATTERNS_BY_CMO = {
422
- "default": {
423
- "flags": {
422
+ PRIORITY_PATTERNS_BY_CMO = {
423
+ "yes_prep": {
424
+ # Patterns used to create broad 1/0 priority flags.
425
+ "concept_patterns": {
424
426
  "sibling": [
425
427
  r"\bsibling\s+attending\b",
428
+ r"\bdynamic\s+sa\b",
426
429
  ],
427
430
  "staff": [
428
431
  r"\bemployee\b",
@@ -430,46 +433,95 @@ DEFAULT_PRIORITY_PATTERNS_BY_CMO = {
430
433
  "zoned": [
431
434
  r"\bisd\b",
432
435
  r"\bzoned\s+for\b",
433
- r"\bdynamic\s+sa\b",
434
436
  ],
435
437
  "transfer": [
436
- r"(?:^|\s{2,})transfer(?:\s{2,}|$)",
438
+ r"\btransfer\b",
437
439
  ],
438
440
  },
439
- "repeat_flags": {
440
- "sib_repeat": [
441
+
442
+ # Patterns used to flag repeated concepts.
443
+ # Example: "Sibling Attending - Sibling Attending Ben Holt"
444
+ # gives sibling_repeat = 1.
445
+ "repeat_patterns": {
446
+ "sibling_repeat": [
441
447
  r"\bsibling\s+attending\b",
448
+ r"\bdynamic\s+sa\b",
442
449
  ],
443
450
  "zoned_repeat": [
444
451
  r"\bzoned\s+for\b",
445
452
  ],
453
+ "transfer_repeat": [
454
+ r"\btransfer\b",
455
+ ],
456
+ },
457
+
458
+ # Patterns used to normalize each dash/plus-separated component.
459
+ # Anything unmatched is preserved as lowercased text.
460
+ "component_replacements": {
461
+ "sibling": [
462
+ r"\bsibling\s+attending(?:\s+.+)?\b",
463
+ r"\bdynamic\s+sa\b",
464
+ ],
465
+ "staff": [
466
+ r"\bemployee\b",
467
+ ],
468
+ "zoned": [
469
+ r"\bisd\b",
470
+ r"\bzoned\s+for\s+.+\b",
471
+ ],
472
+ "transfer": [
473
+ r"\btransfer\b",
474
+ ],
446
475
  },
447
476
  },
448
477
 
449
- # Same as default for now, but this gives you a place to customize later.
450
- "yes_prep": {
451
- "flags": {
478
+ "aspire": {
479
+ "concept_patterns": {
452
480
  "sibling": [
453
481
  r"\bsibling\s+attending\b",
482
+ r"\bmultiples\b",
483
+ r"\bmutiples\b",
454
484
  ],
455
485
  "staff": [
456
486
  r"\bemployee\b",
457
487
  ],
458
488
  "zoned": [
459
- r"\bisd\b",
460
- r"\bzoned\s+for\b",
461
- r"\bdynamic\s+sa\b",
489
+ r"\bin\s+district\b",
462
490
  ],
463
491
  "transfer": [
464
- r"(?:^|\s{2,})transfer(?:\s{2,}|$)",
492
+ r"\btransfer\b",
465
493
  ],
466
494
  },
467
- "repeat_flags": {
468
- "sib_repeat": [
495
+
496
+ "repeat_patterns": {
497
+ "sibling_repeat": [
469
498
  r"\bsibling\s+attending\b",
499
+ r"\bmultiples\b",
500
+ r"\bmutiples\b",
501
+ ],
502
+ "transfer_repeat": [
503
+ r"\btransfer\b",
470
504
  ],
471
505
  "zoned_repeat": [
472
- r"\bzoned\s+for\b",
506
+ r"\bin\s+district\b",
507
+ ],
508
+ },
509
+
510
+ "component_replacements": {
511
+ "sibling": [
512
+ r"\bsibling\s+attending(?:\s+.+)?\b",
513
+ r"\bmultiples\b",
514
+ r"\bmutiples\b",
515
+ ],
516
+ "staff": [
517
+ r"\bemployee\b",
518
+ ],
519
+ "zoned": [
520
+ r"\bin\s+district\b",
521
+ ],
522
+ "transfer": [
523
+ r"\b(?:intra\s+)?aspire\s+transfer(?::?\s+.+)?\b",
524
+ r"\btransfer(?::?\s+.+)?\b",
473
525
  ],
474
526
  },
475
527
  },
@@ -479,22 +531,11 @@ DEFAULT_PRIORITY_PATTERNS_BY_CMO = {
479
531
  def add_priority_flags(
480
532
  df: Union[pl.DataFrame, pl.LazyFrame],
481
533
  priority_col: str,
482
- cmo: str = "default",
534
+ cmo: str,
483
535
  patterns_by_cmo: Optional[Mapping[str, Mapping[str, Mapping[str, Sequence[str]]]]] = None,
536
+ cleaned_col: str = "priority_group_clean",
484
537
  ) -> Union[pl.DataFrame, pl.LazyFrame]:
485
- """Create priority flags from a long priority string column.
486
-
487
- Adds:
488
- sibling
489
- staff
490
- zoned
491
- transfer
492
- sib_repeat
493
- zoned_repeat
494
-
495
- The helper lowercases the priority string and normalizes dashes/pluses to
496
- spaces before matching regex patterns.
497
- """
538
+ """Create broad priority flags and a normalized priority string."""
498
539
  if not isinstance(df, (pl.DataFrame, pl.LazyFrame)):
499
540
  raise TypeError("df must be a polars DataFrame or LazyFrame.")
500
541
 
@@ -502,7 +543,7 @@ def add_priority_flags(
502
543
  if priority_col not in columns:
503
544
  raise ValueError("Missing priority column: %s" % priority_col)
504
545
 
505
- patterns_by_cmo = patterns_by_cmo or DEFAULT_PRIORITY_PATTERNS_BY_CMO
546
+ patterns_by_cmo = patterns_by_cmo or PRIORITY_PATTERNS_BY_CMO
506
547
  if cmo not in patterns_by_cmo:
507
548
  raise ValueError(
508
549
  "No priority pattern mapping found for cmo=%r. Available CMOs: %s"
@@ -510,56 +551,101 @@ def add_priority_flags(
510
551
  )
511
552
 
512
553
  config = patterns_by_cmo[cmo]
513
- flag_patterns = config.get("flags", {})
514
- repeat_patterns = config.get("repeat_flags", {})
554
+ concept_patterns = config.get("concept_patterns", {})
555
+ repeat_patterns = config.get("repeat_patterns", {})
556
+ component_replacements = config.get("component_replacements", {})
515
557
 
516
- def _combined_regex(patterns: Sequence[str]) -> str:
517
- return "|".join("(%s)" % pattern for pattern in patterns)
558
+ def _contains_any(patterns: Sequence[str]) -> pl.Expr:
559
+ if not patterns:
560
+ return pl.lit(False)
561
+ return pl.any_horizontal(
562
+ [pl.col("_priority_text").str.contains(pattern) for pattern in patterns]
563
+ )
518
564
 
519
- def _count_expr(patterns: Sequence[str]) -> pl.Expr:
565
+ def _count_any(patterns: Sequence[str]) -> pl.Expr:
520
566
  if not patterns:
521
567
  return pl.lit(0)
522
568
  return pl.sum_horizontal(
523
- [
524
- pl.col("_priority_text").str.count_matches(pattern)
525
- for pattern in patterns
526
- ]
569
+ [pl.col("_priority_text").str.count_matches(pattern) for pattern in patterns]
527
570
  )
528
571
 
572
+ def _clean_component(value: object) -> Optional[str]:
573
+ if value is None:
574
+ return None
575
+
576
+ text = str(value).strip().lower()
577
+ if not text:
578
+ return None
579
+
580
+ text = re.sub(r"\s+", " ", text)
581
+
582
+ for clean_label, patterns in component_replacements.items():
583
+ for pattern in patterns:
584
+ if re.search(pattern, text):
585
+ return clean_label
586
+
587
+ return text
588
+
589
+ def _normalize_priority(value: object) -> Optional[str]:
590
+ if value is None:
591
+ return None
592
+
593
+ text = str(value).lower()
594
+ text = re.sub(r"[-+]", "|", text)
595
+ raw_parts = [part.strip() for part in text.split("|")]
596
+
597
+ cleaned_parts = []
598
+ seen = set()
599
+ for part in raw_parts:
600
+ cleaned = _clean_component(part)
601
+ if cleaned is None or cleaned in seen:
602
+ continue
603
+ cleaned_parts.append(cleaned)
604
+ seen.add(cleaned)
605
+
606
+ if not cleaned_parts:
607
+ return None
608
+
609
+ return " - ".join(sorted(cleaned_parts))
610
+
529
611
  result = df.with_columns(
530
612
  pl.col(priority_col)
531
613
  .cast(pl.Utf8, strict=False)
532
614
  .fill_null("")
533
615
  .str.to_lowercase()
534
- .str.replace_all(r"[-+]", " ")
535
616
  .alias("_priority_text")
536
617
  )
537
618
 
538
619
  result = result.with_columns(
539
620
  [
540
- pl.col("_priority_text")
541
- .str.contains(_combined_regex(patterns))
542
- .cast(pl.Int8)
543
- .alias(flag_name)
544
- for flag_name, patterns in flag_patterns.items()
621
+ _contains_any(patterns).cast(pl.Int8).alias(flag_name)
622
+ for flag_name, patterns in concept_patterns.items()
545
623
  ]
546
624
  )
547
625
 
548
626
  result = result.with_columns(
549
627
  [
550
- (_count_expr(patterns) > 1)
551
- .cast(pl.Int8)
552
- .alias(flag_name)
628
+ (_count_any(patterns) > 1).cast(pl.Int8).alias(flag_name)
553
629
  for flag_name, patterns in repeat_patterns.items()
554
630
  ]
555
631
  )
556
632
 
557
- return result.drop("_priority_text")
633
+ result = result.with_columns(
634
+ pl.col(priority_col)
635
+ .map_elements(_normalize_priority, return_dtype=pl.Utf8)
636
+ .alias(cleaned_col)
637
+ )
558
638
 
639
+ return result.drop("_priority_text")
559
640
 
560
641
 
561
642
  df = df.pipe(
562
643
  add_priority_flags,
563
644
  priority_col="priority_group_name",
564
645
  cmo="yes_prep",
646
+ )
647
+ df = df.pipe(
648
+ add_priority_flags,
649
+ priority_col="priority_group_name",
650
+ cmo="aspire",
565
651
  )
File without changes