c2r-collect 0.1.0__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 (52) hide show
  1. c2r_collect-0.1.0/MANIFEST.in +1 -0
  2. c2r_collect-0.1.0/PKG-INFO +34 -0
  3. c2r_collect-0.1.0/README.md +18 -0
  4. c2r_collect-0.1.0/pyproject.toml +34 -0
  5. c2r_collect-0.1.0/setup.cfg +4 -0
  6. c2r_collect-0.1.0/src/c2r_collect/__init__.py +13 -0
  7. c2r_collect-0.1.0/src/c2r_collect/__main__.py +6 -0
  8. c2r_collect-0.1.0/src/c2r_collect/cli.py +183 -0
  9. c2r_collect-0.1.0/src/c2r_collect/codebook.py +107 -0
  10. c2r_collect-0.1.0/src/c2r_collect/collect.py +307 -0
  11. c2r_collect-0.1.0/src/c2r_collect/config.py +149 -0
  12. c2r_collect-0.1.0/src/c2r_collect/counters.py +33 -0
  13. c2r_collect-0.1.0/src/c2r_collect/db.py +104 -0
  14. c2r_collect-0.1.0/src/c2r_collect/discover.py +28 -0
  15. c2r_collect-0.1.0/src/c2r_collect/make_input.py +146 -0
  16. c2r_collect-0.1.0/src/c2r_collect/queries/dependencies.sql +5 -0
  17. c2r_collect-0.1.0/src/c2r_collect/queries/dict_columns.sql +12 -0
  18. c2r_collect-0.1.0/src/c2r_collect/queries/dict_constraints.sql +6 -0
  19. c2r_collect-0.1.0/src/c2r_collect/queries/dict_manifest.sql +12 -0
  20. c2r_collect-0.1.0/src/c2r_collect/queries/dict_tables.sql +5 -0
  21. c2r_collect-0.1.0/src/c2r_collect/queries/env.sql +7 -0
  22. c2r_collect-0.1.0/src/c2r_collect/queries/invalid_objects.sql +6 -0
  23. c2r_collect-0.1.0/src/c2r_collect/queries/jobs.sql +5 -0
  24. c2r_collect-0.1.0/src/c2r_collect/queries/procedures.sql +5 -0
  25. c2r_collect-0.1.0/src/c2r_collect/queries/sources.sql +5 -0
  26. c2r_collect-0.1.0/src/c2r_collect/queries/synonyms.sql +3 -0
  27. c2r_collect-0.1.0/src/c2r_collect/queries/triggers.sql +4 -0
  28. c2r_collect-0.1.0/src/c2r_collect/report.py +76 -0
  29. c2r_collect-0.1.0/src/c2r_collect/templates/codebook_README.md +11 -0
  30. c2r_collect-0.1.0/src/c2r_collect/templates/collect-report.md +30 -0
  31. c2r_collect-0.1.0/src/c2r_collect/templates/dict_README.md +13 -0
  32. c2r_collect-0.1.0/src/c2r_collect/transfer.py +122 -0
  33. c2r_collect-0.1.0/src/c2r_collect/writers.py +130 -0
  34. c2r_collect-0.1.0/src/c2r_collect.egg-info/PKG-INFO +34 -0
  35. c2r_collect-0.1.0/src/c2r_collect.egg-info/SOURCES.txt +50 -0
  36. c2r_collect-0.1.0/src/c2r_collect.egg-info/dependency_links.txt +1 -0
  37. c2r_collect-0.1.0/src/c2r_collect.egg-info/entry_points.txt +2 -0
  38. c2r_collect-0.1.0/src/c2r_collect.egg-info/requires.txt +12 -0
  39. c2r_collect-0.1.0/src/c2r_collect.egg-info/top_level.txt +1 -0
  40. c2r_collect-0.1.0/tests/conftest.py +79 -0
  41. c2r_collect-0.1.0/tests/test_cli.py +128 -0
  42. c2r_collect-0.1.0/tests/test_codebook.py +58 -0
  43. c2r_collect-0.1.0/tests/test_collect.py +128 -0
  44. c2r_collect-0.1.0/tests/test_config.py +97 -0
  45. c2r_collect-0.1.0/tests/test_counters.py +13 -0
  46. c2r_collect-0.1.0/tests/test_db_querystore.py +39 -0
  47. c2r_collect-0.1.0/tests/test_discover.py +20 -0
  48. c2r_collect-0.1.0/tests/test_make_input.py +124 -0
  49. c2r_collect-0.1.0/tests/test_package.py +5 -0
  50. c2r_collect-0.1.0/tests/test_report.py +44 -0
  51. c2r_collect-0.1.0/tests/test_transfer.py +104 -0
  52. c2r_collect-0.1.0/tests/test_writers.py +108 -0
@@ -0,0 +1 @@
1
+ include tests/conftest.py
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: c2r-collect
3
+ Version: 0.1.0
4
+ Summary: Oracle data dictionary + PL/SQL source collector that writes okf-loom input folders
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: oracledb>=2.0
9
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
10
+ Provides-Extra: s3
11
+ Requires-Dist: boto3>=1.34; extra == "s3"
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == "dev"
14
+ Requires-Dist: build; extra == "dev"
15
+ Requires-Dist: twine; extra == "dev"
16
+
17
+ # c2r-collect
18
+
19
+ Pulls PL/SQL package sources, relationship CSVs, dictionary and codebook tables from an Oracle
20
+ schema into `raw/<date>/`, then builds an okf-loom input folder from that raw.
21
+
22
+ ```
23
+ pip install c2r-collect # add [s3] extra for upload/download
24
+ cp config.example.toml config.toml # edit; password via env C2R_DB_PASSWORD
25
+ c2r-collect collect # → <root>/raw/<today>/
26
+ c2r-collect upload --date YYYY-MM-DD
27
+ c2r-collect download --date YYYY-MM-DD
28
+ c2r-collect make-input --date YYYY-MM-DD [--prev YYYY-MM-DD] [--add add.txt]
29
+ ```
30
+
31
+ Design and release notes live in the source repository (`docs/`), not in the package.
32
+
33
+ ## First run checks
34
+ See `docs/release.md` (release steps, QA-DB run, VDI production run).
@@ -0,0 +1,18 @@
1
+ # c2r-collect
2
+
3
+ Pulls PL/SQL package sources, relationship CSVs, dictionary and codebook tables from an Oracle
4
+ schema into `raw/<date>/`, then builds an okf-loom input folder from that raw.
5
+
6
+ ```
7
+ pip install c2r-collect # add [s3] extra for upload/download
8
+ cp config.example.toml config.toml # edit; password via env C2R_DB_PASSWORD
9
+ c2r-collect collect # → <root>/raw/<today>/
10
+ c2r-collect upload --date YYYY-MM-DD
11
+ c2r-collect download --date YYYY-MM-DD
12
+ c2r-collect make-input --date YYYY-MM-DD [--prev YYYY-MM-DD] [--add add.txt]
13
+ ```
14
+
15
+ Design and release notes live in the source repository (`docs/`), not in the package.
16
+
17
+ ## First run checks
18
+ See `docs/release.md` (release steps, QA-DB run, VDI production run).
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "c2r-collect"
7
+ version = "0.1.0"
8
+ description = "Oracle data dictionary + PL/SQL source collector that writes okf-loom input folders"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "oracledb>=2.0",
14
+ "tomli>=2.0; python_version < '3.11'",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ s3 = ["boto3>=1.34"]
19
+ dev = ["pytest>=8", "build", "twine"]
20
+
21
+ [project.scripts]
22
+ c2r-collect = "c2r_collect.cli:main"
23
+
24
+ [tool.setuptools]
25
+ package-dir = { "" = "src" }
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
29
+
30
+ [tool.setuptools.package-data]
31
+ c2r_collect = ["queries/*.sql", "templates/*.md"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """c2r-collect: Oracle data dictionary + PL/SQL package sources -> okf-loom input folders.
2
+
3
+ PURPOSE Replace manual DBA spool requests. Pull everything from one Oracle schema into a dated
4
+ raw/ folder, then derive the folder okf-loom (a separate, frozen tool) reads.
5
+ ENTRY console script `c2r-collect` -> c2r_collect.cli:main. Run `c2r-collect --help`.
6
+ PIPELINE collect (DB -> raw/<date>/) -> [upload -> download] -> make-input (raw -> input/<date>/)
7
+ -> okf-loom enrich/build (outside this package).
8
+ MODULES cli (args) . config (config.toml) . db (ONLY module touching Oracle) . queries/*.sql
9
+ . codebook (dynamic SQL) . writers (file contract) . counters . report . collect
10
+ . make_input . transfer (S3) . discover
11
+ EXIT 0 ok . 2 input/config error or partial success . 3 DB/required output failed . 4 transfer failed
12
+ """
13
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """`python -m c2r_collect ...` == `c2r-collect ...` (works even when Scripts/ is not on PATH, e.g. pip --user on VDI)."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ sys.exit(main())
@@ -0,0 +1,183 @@
1
+ """Command line interface. Parses arguments and wires modules; holds no business logic.
2
+
3
+ PURPOSE One entry point for humans, schedulers and LLM agents (`c2r-collect <command>`).
4
+ INPUTS config file (--config PATH, default ./config.toml; may be given before or after the command)
5
+ env C2R_DB_PASSWORD (collect, discover-codebook; prompts if unset)
6
+ env AWS_* or EC2 instance role (upload, download; read by boto3, never by this code)
7
+ OUTPUTS see each run_* function. Console output is ASCII-only; the LAST line is either
8
+ `ok: <path> ...` or `error: <reason>`, so a caller can parse it.
9
+ EXIT 0 ok . 2 input/config error, or collect finished with non-required errors
10
+ . 3 DB connect/required output failed . 4 S3 transfer failed
11
+ NOT HERE any decision about what to collect or select (collect.py, make_input.py).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import datetime as dt
17
+ import os
18
+ import platform
19
+ import re
20
+ import sys
21
+ import traceback
22
+ from pathlib import Path
23
+ from typing import List, Optional
24
+
25
+ from . import __version__
26
+ from .collect import EXIT_DB, EXIT_INPUT, run_collect
27
+ from .config import Config, ConfigError, load_config, resolve_password
28
+ from .db import DbError
29
+ from .discover import run_discover
30
+ from .make_input import run_make_input
31
+ from .transfer import make_client, run_download, run_upload
32
+
33
+ DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
34
+ DEFAULT_CONFIG = "config.toml"
35
+
36
+ # ASCII only: Windows cp949 consoles (VDI) garble arrows and dashes.
37
+ EPILOG = """\
38
+ PIPELINE (one cycle; <root> = config [paths] root):
39
+ 1. collect DB -> <root>/raw/<date>/ pulls ALL packages, CSVs, dictionary, codebook
40
+ 2. upload <root>/raw/<date>/ -> s3://<bucket>/<prefix>/<date>/ (when collect ran on VDI)
41
+ 3. download s3 -> <root>/raw/<date>/ (on the build PC)
42
+ 4. make-input <root>/raw/<date>/ -> <root>/input/<date>/ okf-loom input
43
+ selects packages with a previous description (--prev) plus --add names
44
+ 5. okf-loom enrich/build --input <root>/input/<date> (separate tool, not run here)
45
+
46
+ OUTPUTS:
47
+ raw/<date>/collect-report.md read this first after collect (errors, counts, '?' evidence)
48
+ raw/<date>/MANIFEST.sha256 integrity; verified by upload/download
49
+ input/<date>/make-input-report.md selected / added / vanished packages
50
+
51
+ ENVIRONMENT:
52
+ C2R_DB_PASSWORD DB password for collect and discover-codebook (prompted if unset)
53
+ AWS_* upload/download credentials (SSO portal keys); EC2 uses its instance role
54
+
55
+ EXIT CODES:
56
+ 0 ok
57
+ 2 input or config error; or collect finished but a non-required file failed (see report)
58
+ 3 cannot connect, or a required output failed (raw/<date>.partial/ kept for inspection)
59
+ 4 S3 transfer failed (credentials, AccessDenied, network, manifest mismatch)
60
+
61
+ LAST LINE of output is always one of:
62
+ ok: <path> ... exit 0
63
+ partial: <path> - N problem(s) ... exit 2 (collect wrote raw/<date>/ but some files are missing)
64
+ error: <reason> exit 2, 3 or 4
65
+ """
66
+
67
+
68
+ def _default_db_factory(cfg: Config, password: str):
69
+ from .db import OracleDb
70
+ return OracleDb(cfg.db.dsn, cfg.db.user, password)
71
+
72
+
73
+ def _driver_version() -> str:
74
+ try:
75
+ import oracledb
76
+ return f"python-oracledb {oracledb.__version__}"
77
+ except Exception: # pragma: no cover
78
+ return "python-oracledb (not installed)"
79
+
80
+
81
+ def build_parser() -> argparse.ArgumentParser:
82
+ """argparse parser. The epilog is the machine-oriented manual shown by --help."""
83
+ p = argparse.ArgumentParser(prog="c2r-collect", description="Oracle -> okf-loom input collector",
84
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter)
85
+ p.add_argument("--version", action="version", version=f"c2r-collect {__version__}")
86
+ p.add_argument("--config", dest="config_top", metavar="PATH", default=None, help=f"path to config (default ./{DEFAULT_CONFIG})")
87
+ sub = p.add_subparsers(dest="cmd", required=True)
88
+
89
+ def sp(name, help_, dated=True):
90
+ s = sub.add_parser(name, help=help_)
91
+ s.add_argument("--config", dest="config_sub", default=None, help=argparse.SUPPRESS)
92
+ if dated:
93
+ s.add_argument("--date", default=dt.date.today().isoformat(), help="YYYY-MM-DD (default today)")
94
+ s.add_argument("--force", action="store_true", help="overwrite an existing output folder")
95
+ return s
96
+
97
+ sp("collect", "DB -> raw/<date>/")
98
+ mi = sp("make-input", "raw/<date>/ -> input/<date>/ for okf-loom")
99
+ mi.add_argument("--prev", help="previous input date whose enrichments/ to carry (default: latest before --date)")
100
+ mi.add_argument("--add", help="file path or http(s) URL listing extra package names")
101
+ sp("upload", "raw/<date>/ -> S3")
102
+ sp("download", "S3 -> raw/<date>/")
103
+ sp("discover-codebook", "list candidate codebook tables by name pattern", dated=False)
104
+ return p
105
+
106
+
107
+ def main(argv: Optional[List[str]] = None, db_factory=_default_db_factory, client_factory=make_client,
108
+ env=None) -> int:
109
+ try:
110
+ return _main(argv, db_factory, client_factory, env)
111
+ except SystemExit:
112
+ raise
113
+ except Exception as e: # keep the last-line contract even for bugs, disk full, locked files
114
+ traceback.print_exc(file=sys.stderr)
115
+ print(f"error: unexpected {type(e).__name__}: {str(e).splitlines()[0] if str(e) else ''}")
116
+ return EXIT_DB
117
+
118
+
119
+ def _main(argv, db_factory, client_factory, env) -> int:
120
+ """Run one command and return its exit code (also used as the process exit status).
121
+
122
+ Args:
123
+ argv: arguments without the program name; None means sys.argv[1:].
124
+ db_factory: (Config, password) -> Db. Tests inject FakeDb here.
125
+ client_factory: (region) -> boto3-like S3 client. Tests inject a fake.
126
+ env: environment mapping; None means os.environ.
127
+ Returns:
128
+ 0, 2, 3 or 4 (see module docstring). Never raises for expected failures.
129
+ """
130
+ for stream in (sys.stdout, sys.stderr):
131
+ if hasattr(stream, "reconfigure"):
132
+ stream.reconfigure(errors="replace") # cp949 consoles must not crash on a stray character
133
+ args = build_parser().parse_args(argv)
134
+ env = os.environ if env is None else env
135
+ cfg_path = Path(args.config_sub or args.config_top or DEFAULT_CONFIG)
136
+ try:
137
+ cfg = load_config(cfg_path)
138
+ except ConfigError as e:
139
+ print(f"error: {e}")
140
+ return EXIT_INPUT
141
+ date = getattr(args, "date", None)
142
+ if date is not None and not DATE_RE.match(date):
143
+ print(f"error: --date must be YYYY-MM-DD, got {date!r}")
144
+ return EXIT_INPUT
145
+
146
+ if args.cmd in ("collect", "discover-codebook"):
147
+ try:
148
+ pw = resolve_password(env)
149
+ except ConfigError as e:
150
+ print(f"error: {e}")
151
+ return EXIT_INPUT
152
+ try:
153
+ db = db_factory(cfg, pw)
154
+ except DbError as e:
155
+ print(f"error: cannot connect: {e}")
156
+ return EXIT_DB
157
+ try:
158
+ if args.cmd == "collect":
159
+ return run_collect(cfg, db, date, force=args.force, host=platform.node(), driver=_driver_version())
160
+ return run_discover(cfg, db)
161
+ finally:
162
+ db.close()
163
+
164
+ if args.cmd == "make-input":
165
+ return run_make_input(cfg.root, date, cfg.scope.source_owner, args.prev, args.add, force=args.force)
166
+
167
+ if cfg.s3 is None:
168
+ print("error: [s3] section missing in config")
169
+ return EXIT_INPUT
170
+ try:
171
+ client = client_factory(cfg.s3.region)
172
+ except ImportError:
173
+ print("error: boto3 not installed - pip install 'c2r-collect[s3]'")
174
+ return EXIT_INPUT
175
+ if args.cmd == "upload":
176
+ return run_upload(cfg.root, date, cfg.s3, client)
177
+ if args.force:
178
+ print("note: --force is ignored for download (an existing raw/<date>/ is never overwritten)")
179
+ return run_download(cfg.root, date, cfg.s3, client)
180
+
181
+
182
+ if __name__ == "__main__": # pragma: no cover
183
+ sys.exit(main())
@@ -0,0 +1,107 @@
1
+ """SQL builders for codebook (parameter/code) tables and for discover-codebook.
2
+
3
+ PURPOSE Codebook queries need table names in the SQL text (Oracle cannot bind identifiers).
4
+ SECURITY every owner/table name passes check_identifier() before being inlined:
5
+ ^[A-Z][A-Z0-9_$#]{0,127}$ after upper-casing. Anything else raises ValueError.
6
+ No other value is ever inlined; there are no user-supplied literals.
7
+ OUTPUTS SQL strings only. Execution happens in collect.py / discover.py via Db.query_sql.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from typing import Sequence
13
+
14
+ IDENT = re.compile(r"^[A-Z][A-Z0-9_$#]{0,127}$")
15
+ PATTERN = re.compile(r"^[A-Z0-9_$#%]{1,128}$")
16
+
17
+ # Generic, vendor-neutral defaults. Override per site in config [scope] discover_include / discover_exclude.
18
+ DEFAULT_DISCOVER_INCLUDE = ("%CODE%", "%PARAM%", "%LABEL%", "%LANG%", "%MESSAGE%", "%LIST%", "%TYPE%", "REF%", "%REF")
19
+ DEFAULT_DISCOVER_EXCLUDE = ("TMP%", "STG%", "TEST%", "%BKP%", "%BACKUP%", "%OLD")
20
+
21
+
22
+ def check_identifier(s: str) -> str:
23
+ """Upper-case and validate an Oracle identifier before it is inlined into SQL.
24
+
25
+ Raises:
26
+ ValueError: not ^[A-Z][A-Z0-9_$#]{0,127}$ (blocks injection such as "T; DROP").
27
+ """
28
+ u = (s or "").upper()
29
+ if not IDENT.match(u):
30
+ raise ValueError(f"invalid Oracle identifier: {s!r}")
31
+ return u
32
+
33
+
34
+ def check_pattern(s: str) -> str:
35
+ """Validate a LIKE pattern from config before inlining (letters, digits, _ $ # % only)."""
36
+ u = (s or "").upper()
37
+ if not PATTERN.match(u):
38
+ raise ValueError(f"invalid LIKE pattern: {s!r}")
39
+ return u
40
+
41
+
42
+ def _names(tables: Sequence[str]) -> list:
43
+ names = [check_identifier(t) for t in tables]
44
+ if not names:
45
+ raise ValueError("codebook table list is empty")
46
+ return names
47
+
48
+
49
+ def _in_list(tables: Sequence[str]) -> str:
50
+ return ", ".join(f"'{n}'" for n in _names(tables))
51
+
52
+
53
+ def _name_rows(tables: Sequence[str]) -> str:
54
+ return " UNION ALL ".join(f"SELECT '{n}' AS table_name FROM dual" for n in _names(tables))
55
+
56
+
57
+ def sql_exists(owner: str, tables: Sequence[str]) -> str:
58
+ o = check_identifier(owner)
59
+ return (
60
+ "SELECT g.table_name, "
61
+ "CASE WHEN t.table_name IS NULL THEN 'MISSING' ELSE 'EXISTS' END AS status, "
62
+ "t.num_rows AS stat_rows, t.last_analyzed, "
63
+ f"(SELECT COUNT(*) FROM all_tab_columns c WHERE c.owner = '{o}' AND c.table_name = g.table_name) AS col_cnt "
64
+ f"FROM ({_name_rows(tables)}) g "
65
+ f"LEFT JOIN all_tables t ON t.owner = '{o}' AND t.table_name = g.table_name "
66
+ "ORDER BY g.table_name"
67
+ )
68
+
69
+
70
+ def sql_rowcount_one(owner: str, table: str) -> str:
71
+ """COUNT(*) for one table. One statement per table so a missing table loses only its own row."""
72
+ o, n = check_identifier(owner), check_identifier(table)
73
+ return f"SELECT '{n}' AS table_name, COUNT(*) AS real_rows FROM {o}.{n}"
74
+
75
+
76
+ def sql_owners(tables: Sequence[str]) -> str:
77
+ return ("SELECT t.owner, t.table_name, t.num_rows AS stat_rows, t.last_analyzed "
78
+ f"FROM all_tables t WHERE t.table_name IN ({_in_list(tables)}) "
79
+ "ORDER BY t.table_name, t.owner")
80
+
81
+
82
+ def sql_columns(owner: str, tables: Sequence[str]) -> str:
83
+ o = check_identifier(owner)
84
+ return ("SELECT c.table_name, c.column_id, c.column_name, c.data_type, c.data_length, c.nullable, cc.comments "
85
+ "FROM all_tab_columns c "
86
+ "LEFT JOIN all_col_comments cc ON cc.owner = c.owner AND cc.table_name = c.table_name "
87
+ "AND cc.column_name = c.column_name "
88
+ f"WHERE c.owner = '{o}' AND c.table_name IN ({_in_list(tables)}) "
89
+ "ORDER BY c.table_name, c.column_id")
90
+
91
+
92
+ def sql_dump(owner: str, table: str) -> str:
93
+ return f"SELECT * FROM {check_identifier(owner)}.{check_identifier(table)}"
94
+
95
+
96
+ def sql_discover(owner: str, include: Sequence[str] = DEFAULT_DISCOVER_INCLUDE,
97
+ exclude: Sequence[str] = DEFAULT_DISCOVER_EXCLUDE) -> str:
98
+ o = check_identifier(owner)
99
+ inc = [check_pattern(p) for p in include] or ["%"]
100
+ exc = [check_pattern(p) for p in exclude]
101
+ inc_sql = " OR ".join(f"UPPER(t.table_name) LIKE '{p}'" for p in inc)
102
+ exc_sql = " AND ".join(f"t.table_name NOT LIKE '{p}'" for p in exc) or "1 = 1"
103
+ return ("SELECT t.table_name, t.num_rows AS stat_rows, "
104
+ "(SELECT COUNT(*) FROM all_tab_columns c WHERE c.owner = t.owner AND c.table_name = t.table_name) AS col_cnt, "
105
+ "(SELECT tc.comments FROM all_tab_comments tc WHERE tc.owner = t.owner AND tc.table_name = t.table_name) AS table_comment "
106
+ f"FROM all_tables t WHERE t.owner = '{o}' AND ({inc_sql}) AND {exc_sql} "
107
+ "ORDER BY t.table_name")