ltc-code 0.1.92__tar.gz → 0.1.94__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.94
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.94"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -413,63 +413,130 @@ 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
+ import re
423
+ from typing import Mapping, Optional, Sequence, Union
424
+
425
+ import polars as pl
426
+
427
+
428
+ PRIORITY_PATTERNS_BY_CMO = {
429
+ "yes_prep": {
430
+ "concept_patterns": {
424
431
  "sibling": [
425
432
  r"\bsibling\s+attending\b",
426
433
  ],
434
+ "sibling_concur": [
435
+ r"\bdynamic\b",
436
+ ],
427
437
  "staff": [
428
438
  r"\bemployee\b",
429
439
  ],
430
440
  "zoned": [
431
441
  r"\bisd\b",
432
442
  r"\bzoned\s+for\b",
433
- r"\bdynamic\s+sa\b",
434
443
  ],
435
444
  "transfer": [
436
- r"(?:^|\s{2,})transfer(?:\s{2,}|$)",
445
+ r"\btransfer\b",
437
446
  ],
438
447
  },
439
- "repeat_flags": {
440
- "sib_repeat": [
448
+
449
+ "repeat_patterns": {
450
+ "sibling_repeat": [
441
451
  r"\bsibling\s+attending\b",
442
452
  ],
453
+ "sibling_concur_repeat": [
454
+ r"\bdynamic\b",
455
+ ],
443
456
  "zoned_repeat": [
444
457
  r"\bzoned\s+for\b",
445
458
  ],
459
+ "transfer_repeat": [
460
+ r"\btransfer\b",
461
+ ],
462
+ },
463
+
464
+ "component_replacements": {
465
+ "sibling": [
466
+ r"\bsibling\s+attending(?:\s+.+)?\b",
467
+ ],
468
+ "sibling_concur": [
469
+ r"\bdynamic(?:\s+.+)?\b",
470
+ ],
471
+ "staff": [
472
+ r"\bemployee\b",
473
+ ],
474
+ "zoned": [
475
+ r"\bisd\b",
476
+ r"\bzoned\s+for\s+.+\b",
477
+ ],
478
+ "transfer": [
479
+ r"\btransfer\b",
480
+ ],
446
481
  },
447
482
  },
448
483
 
449
- # Same as default for now, but this gives you a place to customize later.
450
- "yes_prep": {
451
- "flags": {
484
+ "aspire": {
485
+ "concept_patterns": {
452
486
  "sibling": [
453
487
  r"\bsibling\s+attending\b",
488
+ r"\bmultiples\b",
489
+ r"\bmutiples\b",
490
+ ],
491
+ "sibling_concur": [
492
+ r"\bdynamic\b",
454
493
  ],
455
494
  "staff": [
456
495
  r"\bemployee\b",
457
496
  ],
458
497
  "zoned": [
459
- r"\bisd\b",
460
- r"\bzoned\s+for\b",
461
- r"\bdynamic\s+sa\b",
498
+ r"\bin\s+district\b",
462
499
  ],
463
500
  "transfer": [
464
- r"(?:^|\s{2,})transfer(?:\s{2,}|$)",
501
+ r"\btransfer\b",
465
502
  ],
466
503
  },
467
- "repeat_flags": {
468
- "sib_repeat": [
504
+
505
+ "repeat_patterns": {
506
+ "sibling_repeat": [
469
507
  r"\bsibling\s+attending\b",
508
+ r"\bmultiples\b",
509
+ r"\bmutiples\b",
510
+ ],
511
+ "sibling_concur_repeat": [
512
+ r"\bdynamic\b",
513
+ ],
514
+ "transfer_repeat": [
515
+ r"\btransfer\b",
470
516
  ],
471
517
  "zoned_repeat": [
472
- r"\bzoned\s+for\b",
518
+ r"\bin\s+district\b",
519
+ ],
520
+ },
521
+
522
+ "component_replacements": {
523
+ "sibling": [
524
+ r"\bsibling\s+attending(?:\s+.+)?\b",
525
+ r"\bmultiples\b",
526
+ r"\bmutiples\b",
527
+ ],
528
+ "sibling_concur": [
529
+ r"\bdynamic(?:\s+.+)?\b",
530
+ ],
531
+ "staff": [
532
+ r"\bemployee\b",
533
+ ],
534
+ "zoned": [
535
+ r"\bin\s+district\b",
536
+ ],
537
+ "transfer": [
538
+ r"\b(?:intra\s+)?aspire\s+transfer(?::?\s+.+)?\b",
539
+ r"\btransfer(?::?\s+.+)?\b",
473
540
  ],
474
541
  },
475
542
  },
@@ -479,22 +546,11 @@ DEFAULT_PRIORITY_PATTERNS_BY_CMO = {
479
546
  def add_priority_flags(
480
547
  df: Union[pl.DataFrame, pl.LazyFrame],
481
548
  priority_col: str,
482
- cmo: str = "default",
549
+ cmo: str,
483
550
  patterns_by_cmo: Optional[Mapping[str, Mapping[str, Mapping[str, Sequence[str]]]]] = None,
551
+ cleaned_col: str = "priority_group_clean",
484
552
  ) -> 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
- """
553
+ """Create broad priority flags and a normalized priority string."""
498
554
  if not isinstance(df, (pl.DataFrame, pl.LazyFrame)):
499
555
  raise TypeError("df must be a polars DataFrame or LazyFrame.")
500
556
 
@@ -502,7 +558,7 @@ def add_priority_flags(
502
558
  if priority_col not in columns:
503
559
  raise ValueError("Missing priority column: %s" % priority_col)
504
560
 
505
- patterns_by_cmo = patterns_by_cmo or DEFAULT_PRIORITY_PATTERNS_BY_CMO
561
+ patterns_by_cmo = patterns_by_cmo or PRIORITY_PATTERNS_BY_CMO
506
562
  if cmo not in patterns_by_cmo:
507
563
  raise ValueError(
508
564
  "No priority pattern mapping found for cmo=%r. Available CMOs: %s"
@@ -510,50 +566,91 @@ def add_priority_flags(
510
566
  )
511
567
 
512
568
  config = patterns_by_cmo[cmo]
513
- flag_patterns = config.get("flags", {})
514
- repeat_patterns = config.get("repeat_flags", {})
569
+ concept_patterns = config.get("concept_patterns", {})
570
+ repeat_patterns = config.get("repeat_patterns", {})
571
+ component_replacements = config.get("component_replacements", {})
515
572
 
516
- def _combined_regex(patterns: Sequence[str]) -> str:
517
- return "|".join("(%s)" % pattern for pattern in patterns)
573
+ def _contains_any(patterns: Sequence[str]) -> pl.Expr:
574
+ if not patterns:
575
+ return pl.lit(False)
576
+ return pl.any_horizontal(
577
+ [pl.col("_priority_text").str.contains(pattern) for pattern in patterns]
578
+ )
518
579
 
519
- def _count_expr(patterns: Sequence[str]) -> pl.Expr:
580
+ def _count_any(patterns: Sequence[str]) -> pl.Expr:
520
581
  if not patterns:
521
582
  return pl.lit(0)
522
583
  return pl.sum_horizontal(
523
- [
524
- pl.col("_priority_text").str.count_matches(pattern)
525
- for pattern in patterns
526
- ]
584
+ [pl.col("_priority_text").str.count_matches(pattern) for pattern in patterns]
527
585
  )
528
586
 
587
+ def _clean_component(value: object) -> Optional[str]:
588
+ if value is None:
589
+ return None
590
+
591
+ text = str(value).strip().lower()
592
+ if not text:
593
+ return None
594
+
595
+ text = re.sub(r"\s+", " ", text)
596
+
597
+ for clean_label, patterns in component_replacements.items():
598
+ for pattern in patterns:
599
+ if re.search(pattern, text):
600
+ return clean_label
601
+
602
+ return text
603
+
604
+ def _normalize_priority(value: object) -> Optional[str]:
605
+ if value is None:
606
+ return None
607
+
608
+ text = str(value).lower()
609
+ text = re.sub(r"[-+]", "|", text)
610
+ raw_parts = [part.strip() for part in text.split("|")]
611
+
612
+ cleaned_parts = []
613
+ seen = set()
614
+ for part in raw_parts:
615
+ cleaned = _clean_component(part)
616
+ if cleaned is None or cleaned in seen:
617
+ continue
618
+ cleaned_parts.append(cleaned)
619
+ seen.add(cleaned)
620
+
621
+ if not cleaned_parts:
622
+ return None
623
+
624
+ return " - ".join(sorted(cleaned_parts))
625
+
529
626
  result = df.with_columns(
530
627
  pl.col(priority_col)
531
628
  .cast(pl.Utf8, strict=False)
532
629
  .fill_null("")
533
630
  .str.to_lowercase()
534
- .str.replace_all(r"[-+]", " ")
535
631
  .alias("_priority_text")
536
632
  )
537
633
 
538
634
  result = result.with_columns(
539
635
  [
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()
636
+ _contains_any(patterns).cast(pl.Int8).alias(flag_name)
637
+ for flag_name, patterns in concept_patterns.items()
545
638
  ]
546
639
  )
547
640
 
548
641
  result = result.with_columns(
549
642
  [
550
- (_count_expr(patterns) > 1)
551
- .cast(pl.Int8)
552
- .alias(flag_name)
643
+ (_count_any(patterns) > 1).cast(pl.Int8).alias(flag_name)
553
644
  for flag_name, patterns in repeat_patterns.items()
554
645
  ]
555
646
  )
556
647
 
648
+ result = result.with_columns(
649
+ pl.col(priority_col)
650
+ .map_elements(_normalize_priority, return_dtype=pl.Utf8)
651
+ .alias(cleaned_col)
652
+ )
653
+
557
654
  return result.drop("_priority_text")
558
655
 
559
656
 
@@ -562,4 +659,9 @@ df = df.pipe(
562
659
  add_priority_flags,
563
660
  priority_col="priority_group_name",
564
661
  cmo="yes_prep",
662
+ )
663
+ df = df.pipe(
664
+ add_priority_flags,
665
+ priority_col="priority_group_name",
666
+ cmo="aspire",
565
667
  )
File without changes