simtoolsz 0.2.13__tar.gz → 0.2.14__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.
Files changed (47) hide show
  1. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/PKG-INFO +1 -1
  2. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/pyproject.toml +1 -1
  3. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/__init__.py +3 -1
  4. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/reader.py +257 -1
  5. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.github/workflows/publish.yml +0 -0
  6. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.github/workflows/test.yml +0 -0
  7. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.gitignore +0 -0
  8. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/.python-version +0 -0
  9. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/LICENSE +0 -0
  10. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README.md +0 -0
  11. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README_EN.md +0 -0
  12. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README_countrycode.md +0 -0
  13. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/README_countrycode_en.md +0 -0
  14. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/DATETIME_CONVERSION.md +0 -0
  15. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/iso3166-1.xlsx +0 -0
  16. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/mail_usage_guide.md +0 -0
  17. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/docs/special2db_usage.md +0 -0
  18. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/conversion_examples.py +0 -0
  19. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/mail_examples.py +0 -0
  20. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/special2db_example.py +0 -0
  21. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/today_examples.py +0 -0
  22. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/examples/zip2db_example.py +0 -0
  23. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/requirements-dev.lock +0 -0
  24. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/requirements.lock +0 -0
  25. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/columns_info +0 -0
  26. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/country.parquet +0 -0
  27. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/countrycode.py +0 -0
  28. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/datetime.py +0 -0
  29. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/db.py +0 -0
  30. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/mail.py +0 -0
  31. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/src/simtoolsz/utils.py +0 -0
  32. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_conversion.py +0 -0
  33. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_countrycode.py +0 -0
  34. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_countrycode_optimization.py +0 -0
  35. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_iso_comprehensive.py +0 -0
  36. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_iso_format.py +0 -0
  37. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_optimized_reader.py +0 -0
  38. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_simple.py +0 -0
  39. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_smoke.py +0 -0
  40. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_special2db.py +0 -0
  41. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_special2db_simple.py +0 -0
  42. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_today_optimized.py +0 -0
  43. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_which_format.py +0 -0
  44. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_zip2db.py +0 -0
  45. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/test_zip2db_simple.py +0 -0
  46. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/tests/verify_unicode_fix.py +0 -0
  47. {simtoolsz-0.2.13 → simtoolsz-0.2.14}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: simtoolsz
3
- Version: 0.2.13
3
+ Version: 0.2.14
4
4
  Summary: A simple and convenient toolkit containing useful functions, classes, and methods.
5
5
  Project-URL: Homepage, https://github.com/SidneyLYZhang/simtoolsz
6
6
  Project-URL: Repository, https://github.com/SidneyLYZhang/simtoolsz.git
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simtoolsz"
3
- version = "0.2.13"
3
+ version = "0.2.14"
4
4
  description = "A simple and convenient toolkit containing useful functions, classes, and methods."
5
5
  keywords = ["tool", "collection"]
6
6
  license = { text = "MulanPSL-2.0" }
@@ -25,7 +25,9 @@ import simtoolsz.countrycode as countrycode
25
25
  try:
26
26
  __version__ = importlib.metadata.version("simtoolsz")
27
27
  except importlib.metadata.PackageNotFoundError:
28
- __version__ = "0.2.13"
28
+ __version__ = "0.2.14"
29
+
30
+ __author__ = "Sidney Zhang <zly@lyzhang.me>"
29
31
 
30
32
  __all__ = [
31
33
  '__version__', 'mail', 'utils', 'datetime',
@@ -26,6 +26,7 @@
26
26
  """
27
27
 
28
28
  import warnings
29
+ import io
29
30
  import re
30
31
  import polars as pl
31
32
 
@@ -40,7 +41,7 @@ from tempfile import TemporaryDirectory
40
41
 
41
42
  __all__ = [
42
43
  "read_tsv", "scan_tsv", "getreader", "load_data", "read_archive",
43
- "is_archive_file", "excel_sheet_names", "load_excel"
44
+ "is_archive_file", "excel_sheet_names", "load_excel", "read_csv_advanced"
44
45
  ]
45
46
 
46
47
 
@@ -651,3 +652,258 @@ def load_excel(
651
652
  else:
652
653
  raise ValueError(f"Sheet {sheet_name} not found in Excel file")
653
654
  return df
655
+
656
+
657
+ def read_csv_advanced(
658
+ path: str | Path,
659
+ csv_name: Optional[str] = None,
660
+ start_marker: Optional[str] = "#-------------------------",
661
+ end_marker: Optional[str] = None,
662
+ encoding: str = "utf-8",
663
+ **read_kwargs
664
+ ) -> pl.DataFrame:
665
+ """
666
+ 从 ZIP 压缩包或文件夹中读取被标记行包裹的 CSV 数据。
667
+
668
+ 匹配规则:只要行首(忽略前导空格)以标识符字符串开头即视为边界,
669
+ 例如 "#------------------------- 数据开始" 会被正确识别。
670
+ 支持不同的起始和结束标识符,标识符只需匹配行的前缀即可。
671
+
672
+ Args:
673
+ path: ZIP 文件路径或文件夹路径
674
+ csv_name: ZIP 内或文件夹中的 CSV 文件名(可选,单 CSV 文件时可自动检测)
675
+ start_marker: 起始边界标记的前缀字符串(默认 "#-------------------------")
676
+ 设为 None 则从文件开头读取
677
+ end_marker: 结束边界标记的前缀字符串(默认 None,表示与 start_marker 相同)
678
+ 设为 None 则使用 start_marker 作为结束标记
679
+ encoding: 文件编码(默认 utf-8,中文环境常见 gbk/utf-8-sig)
680
+ **read_kwargs: 传递给 polars.read_csv 的额外参数(如 separator, header 等)
681
+
682
+ Returns:
683
+ pl.DataFrame: 解析后的数据帧
684
+
685
+ Raises:
686
+ FileNotFoundError: 路径不存在
687
+ ValueError: 未找到 CSV 文件或数据区域为空
688
+
689
+ Examples:
690
+ >>> # 从 ZIP 文件读取(默认标记)
691
+ >>> df = read_csv_from_zip("data.zip", encoding="gbk")
692
+
693
+ >>> # 从文件夹读取
694
+ >>> df = read_csv_from_zip("./data_folder", separator="|")
695
+
696
+ >>> # 使用不同的起始和结束标记
697
+ >>> df = read_csv_from_zip("data.zip", start_marker="=== BEGIN ===", end_marker="=== END ===")
698
+
699
+ >>> # 无标记,读取整个文件
700
+ >>> df = read_csv_from_zip("data.zip", start_marker=None)
701
+ """
702
+ path = Path(path)
703
+ if not path.exists():
704
+ raise FileNotFoundError(f"路径不存在: {path}")
705
+
706
+ if end_marker is None:
707
+ end_marker = start_marker
708
+
709
+ content = _read_csv_content(path, csv_name, encoding)
710
+ data_lines = _extract_data_lines(content, start_marker, end_marker)
711
+
712
+ if not data_lines:
713
+ raise ValueError("提取的数据区域为空,请检查标记是否正确")
714
+
715
+ csv_content = "\n".join(data_lines)
716
+
717
+ return pl.read_csv(
718
+ io.BytesIO(csv_content.encode(encoding)),
719
+ **read_kwargs
720
+ )
721
+
722
+
723
+ def _read_csv_content(path: Path, csv_name: Optional[str], encoding: str) -> str:
724
+ """
725
+ 从 ZIP 文件或文件夹中读取 CSV 文件的文本内容。
726
+
727
+ Args:
728
+ path: ZIP 文件路径或文件夹路径
729
+ csv_name: CSV 文件名(可选)
730
+ encoding: 文件编码
731
+
732
+ Returns:
733
+ str: CSV 文件的文本内容
734
+ """
735
+ if path.is_file() and path.suffix.lower() == '.zip':
736
+ return _read_from_zip_file(path, csv_name, encoding)
737
+ elif path.is_dir():
738
+ return _read_from_directory(path, csv_name, encoding)
739
+ else:
740
+ raise ValueError(f"路径必须是 ZIP 文件或文件夹: {path}")
741
+
742
+
743
+ def _read_from_zip_file(zip_path: Path, csv_name: Optional[str], encoding: str) -> str:
744
+ """
745
+ 从 ZIP 文件中读取 CSV 内容。
746
+
747
+ Args:
748
+ zip_path: ZIP 文件路径
749
+ csv_name: CSV 文件名(可选)
750
+ encoding: 文件编码
751
+
752
+ Returns:
753
+ str: CSV 文件的文本内容
754
+ """
755
+ with ZipFile(zip_path, "r") as zf:
756
+ csv_name = _resolve_csv_name_zip(zf, csv_name, zip_path)
757
+ with zf.open(csv_name) as f:
758
+ actual_encoding = "utf-8-sig" if encoding == "utf-8" else encoding
759
+ return f.read().decode(actual_encoding)
760
+
761
+
762
+ def _read_from_directory(dir_path: Path, csv_name: Optional[str], encoding: str) -> str:
763
+ """
764
+ 从文件夹中读取 CSV 内容。
765
+
766
+ Args:
767
+ dir_path: 文件夹路径
768
+ csv_name: CSV 文件名(可选)
769
+ encoding: 文件编码
770
+
771
+ Returns:
772
+ str: CSV 文件的文本内容
773
+ """
774
+ csv_file = _resolve_csv_name_dir(dir_path, csv_name)
775
+ actual_encoding = "utf-8-sig" if encoding == "utf-8" else encoding
776
+ return csv_file.read_text(encoding=actual_encoding)
777
+
778
+
779
+ def _resolve_csv_name_zip(zf, csv_name: Optional[str], zip_path: Path) -> str:
780
+ """
781
+ 解析 ZIP 文件中的 CSV 文件名。
782
+
783
+ Args:
784
+ zf: ZipFile 对象
785
+ csv_name: 指定的 CSV 文件名(可选)
786
+ zip_path: ZIP 文件路径(用于错误信息)
787
+
788
+ Returns:
789
+ str: CSV 文件名
790
+ """
791
+ if csv_name is not None:
792
+ if csv_name not in zf.namelist():
793
+ raise ValueError(f"ZIP 中不存在文件: {csv_name}")
794
+ return csv_name
795
+
796
+ candidates = [
797
+ n for n in zf.namelist()
798
+ if n.lower().endswith(".csv") and not n.startswith("__MACOSX")
799
+ ]
800
+
801
+ if len(candidates) == 0:
802
+ raise ValueError(f"在 {zip_path} 中未找到 CSV 文件")
803
+ if len(candidates) > 1:
804
+ raise ValueError(f"存在多个 CSV 文件,请指定 csv_name: {candidates}")
805
+
806
+ return candidates[0]
807
+
808
+
809
+ def _resolve_csv_name_dir(dir_path: Path, csv_name: Optional[str]) -> Path:
810
+ """
811
+ 解析文件夹中的 CSV 文件路径。
812
+
813
+ Args:
814
+ dir_path: 文件夹路径
815
+ csv_name: 指定的 CSV 文件名(可选)
816
+
817
+ Returns:
818
+ Path: CSV 文件路径
819
+ """
820
+ if csv_name is not None:
821
+ csv_file = dir_path / csv_name
822
+ if not csv_file.exists():
823
+ raise ValueError(f"文件夹中不存在文件: {csv_name}")
824
+ return csv_file
825
+
826
+ candidates = [
827
+ f for f in dir_path.iterdir()
828
+ if f.is_file() and f.suffix.lower() == ".csv"
829
+ ]
830
+
831
+ if len(candidates) == 0:
832
+ raise ValueError(f"在 {dir_path} 中未找到 CSV 文件")
833
+ if len(candidates) > 1:
834
+ raise ValueError(f"存在多个 CSV 文件,请指定 csv_name: {[f.name for f in candidates]}")
835
+
836
+ return candidates[0]
837
+
838
+
839
+ def _extract_data_lines(
840
+ content: str,
841
+ start_marker: Optional[str],
842
+ end_marker: Optional[str]
843
+ ) -> list[str]:
844
+ """
845
+ 从内容中提取数据行。
846
+
847
+ Args:
848
+ content: 文件内容
849
+ start_marker: 起始标记(None 表示从开头开始)
850
+ end_marker: 结束标记(None 表示到结尾结束)
851
+
852
+ Returns:
853
+ list[str]: 数据行列表
854
+ """
855
+ lines = content.splitlines()
856
+
857
+ if start_marker is None and end_marker is None:
858
+ return lines
859
+
860
+ start_idx = _find_marker_index(lines, start_marker, find_first=True)
861
+ end_idx = _find_marker_index(lines, end_marker, find_first=False, start_from=start_idx)
862
+
863
+ if start_idx is None:
864
+ warnings.warn(f"未找到起始标记 '{start_marker}',将解析整个文件内容")
865
+ return lines
866
+
867
+ data_start = start_idx + 1
868
+ data_end = end_idx if end_idx is not None else len(lines)
869
+
870
+ return lines[data_start:data_end]
871
+
872
+
873
+ def _find_marker_index(
874
+ lines: list[str],
875
+ marker: Optional[str],
876
+ find_first: bool = True,
877
+ start_from: Optional[int] = None
878
+ ) -> Optional[int]:
879
+ """
880
+ 查找标记行的索引。
881
+
882
+ Args:
883
+ lines: 行列表
884
+ marker: 标记字符串(匹配行的前缀)
885
+ find_first: True 返回第一个匹配,False 返回最后一个匹配
886
+ start_from: 开始搜索的索引
887
+
888
+ Returns:
889
+ Optional[int]: 标记行的索引,未找到返回 None
890
+ """
891
+ if marker is None:
892
+ return None
893
+
894
+ search_range = range(
895
+ start_from + 1 if start_from is not None else 0,
896
+ len(lines)
897
+ )
898
+
899
+ if find_first:
900
+ for i in search_range:
901
+ if lines[i].lstrip().startswith(marker):
902
+ return i
903
+ return None
904
+ else:
905
+ result = None
906
+ for i in search_range:
907
+ if lines[i].lstrip().startswith(marker):
908
+ result = i
909
+ return result
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes