echoss-db 2.2.1__tar.gz → 2.3.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.
- {echoss_db-2.2.1/echoss_db.egg-info → echoss_db-2.3.0}/PKG-INFO +43 -6
- {echoss_db-2.2.1 → echoss_db-2.3.0}/README.md +39 -3
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/elastic_search.py +5 -3
- echoss_db-2.3.0/echoss_db/env_config.py +53 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mongo_query.py +2 -2
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mysql_query.py +4 -3
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/postgres_query.py +4 -3
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/qdrant_vector.py +28 -5
- {echoss_db-2.2.1 → echoss_db-2.3.0/echoss_db.egg-info}/PKG-INFO +43 -6
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db.egg-info/SOURCES.txt +4 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db.egg-info/requires.txt +3 -2
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db.egg-info/top_level.txt +1 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/pyproject.toml +4 -3
- {echoss_db-2.2.1 → echoss_db-2.3.0}/requirements.txt +2 -2
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/integration/conftest.py +4 -4
- echoss_db-2.3.0/tests/integration/test_env_config_integration.py +71 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/integration/test_qdrant_integration.py +63 -0
- echoss_db-2.3.0/tests/unit/test_env_config.py +65 -0
- echoss_db-2.3.0/tests/unit/test_qdrant_role.py +30 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/LICENSE +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/MANIFEST.in +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/dataclass_mapper.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/errors.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/param_mapper.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/pydantic_mapper.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/row_mapper.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/mapping/types.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db/sql_transaction.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/echoss_db.egg-info/dependency_links.txt +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/package_tests/test_package_imports.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/setup.cfg +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/integration/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/integration/test_postgres_integration.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/_postgres_test_schema.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_elasticsearch.ipynb +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_elasticsearch.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_mapping_mysql.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_mongo.ipynb +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_mongo.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_mysql.ipynb +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_mysql.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_postgres.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_postgres_qdrant_ingest.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/example_qdrant.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/manual/setup_ai_rag_test_schema.sql +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/unit/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/unit/mapping/__init__.py +0 -0
- {echoss_db-2.2.1 → echoss_db-2.3.0}/tests/unit/mapping/test_param_mapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echoss-db
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: echoss AI Bigdata Solution - Database Query Package
|
|
5
5
|
Author-email: ckkim <ckkim@12cm.co.kr>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -13,13 +13,14 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: pandas>=
|
|
16
|
+
Requires-Dist: pandas>=3.0
|
|
17
17
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
18
18
|
Requires-Dist: PyMySQL>=1.0.2
|
|
19
19
|
Requires-Dist: opensearch-py<3.0.0,>=2.8.0
|
|
20
|
-
Requires-Dist: echoss-
|
|
20
|
+
Requires-Dist: echoss-common<3,>=1.2.0
|
|
21
21
|
Requires-Dist: psycopg[binary]<4.0.0,>=3.2
|
|
22
22
|
Requires-Dist: qdrant-client[fastembed]<2.0.0,>=1.14.1
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
23
24
|
Provides-Extra: mapping
|
|
24
25
|
Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "mapping"
|
|
25
26
|
Provides-Extra: mongo
|
|
@@ -36,16 +37,24 @@ MySQL, PostgreSQL, OpenSearch, Qdrant compatible query/vector access package
|
|
|
36
37
|
> ⚠️ **MongoDB support is deprecated** (v2.2.0) and will be removed in v3.0.0.
|
|
37
38
|
> `pymongo` is now an optional dependency: `pip install echoss-db[mongo]`.
|
|
38
39
|
|
|
40
|
+
> 작업 이력(완료된 결론·미착수 백로그)은 [`prior-work.jsonl`](./prior-work.jsonl)에 기록됩니다.
|
|
41
|
+
> 조회: `/prior-work <주제어>` (예: `qdrant`, `.env`, `mysql passwd`).
|
|
42
|
+
|
|
39
43
|
## Prepare
|
|
40
44
|
|
|
41
45
|
사용 전 config(인증 정보) 유무를 확인한 뒤 사용해야 합니다.
|
|
42
46
|
`config/config.example.yaml`을 `config/config.yaml`로 복사해 실제 값을 채우세요 (실 config는 gitignore 대상).
|
|
43
47
|
`config/config.yaml` 기준의 credential 제거 예시는 아래와 같습니다.
|
|
44
48
|
|
|
49
|
+
credential 필드(`passwd`, `api_key`, `read_only_api_key`)는 평문 대신 `${VAR_NAME}` 형태로
|
|
50
|
+
적으면 `.env`(또는 OS 환경변수)에서 값을 읽어 채웁니다. `.env`는 git에 추적되지 않으므로
|
|
51
|
+
`.env.example`을 `.env`로 복사해 실제 값을 채우세요. host/port 등 credential이 아닌 필드는
|
|
52
|
+
지금처럼 평문 값을 직접 씁니다.
|
|
53
|
+
|
|
45
54
|
```yaml
|
|
46
55
|
mysql:
|
|
47
56
|
user: <MYSQL_USER>
|
|
48
|
-
passwd:
|
|
57
|
+
passwd: ${MYSQL_PASSWD}
|
|
49
58
|
host: <MYSQL_HOST>
|
|
50
59
|
port: <MYSQL_PORT>
|
|
51
60
|
db: <MYSQL_DB>
|
|
@@ -58,7 +67,7 @@ mongo:
|
|
|
58
67
|
|
|
59
68
|
elastic:
|
|
60
69
|
user: <ELASTIC_USER>
|
|
61
|
-
passwd:
|
|
70
|
+
passwd: ${ELASTIC_PASSWD}
|
|
62
71
|
host: <ELASTIC_HOST>
|
|
63
72
|
port: <ELASTIC_PORT>
|
|
64
73
|
scheme: <http|https>
|
|
@@ -66,7 +75,7 @@ elastic:
|
|
|
66
75
|
|
|
67
76
|
postgres:
|
|
68
77
|
user: <POSTGRES_USER>
|
|
69
|
-
passwd:
|
|
78
|
+
passwd: ${POSTGRES_PASSWD}
|
|
70
79
|
host: <POSTGRES_HOST>
|
|
71
80
|
port: <POSTGRES_PORT>
|
|
72
81
|
db: <POSTGRES_DB>
|
|
@@ -81,8 +90,31 @@ qdrant:
|
|
|
81
90
|
default_limit: <DEFAULT_LIMIT>
|
|
82
91
|
# fastembed_model omitted -> default FastEmbed model automatically applied
|
|
83
92
|
# fastembed_model: null -> external vector mode (no FastEmbed)
|
|
93
|
+
api_key: ${QDRANT_API_KEY} # optional, admin/전체 권한
|
|
94
|
+
read_only_api_key: ${QDRANT_READ_ONLY_API_KEY} # optional, 조회 전용
|
|
84
95
|
```
|
|
85
96
|
|
|
97
|
+
`.env` 예시(`.env.example` 참고, 실값은 커밋하지 않음):
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
MYSQL_PASSWD=
|
|
101
|
+
ELASTIC_PASSWD=
|
|
102
|
+
POSTGRES_PASSWD=
|
|
103
|
+
QDRANT_API_KEY=
|
|
104
|
+
QDRANT_READ_ONLY_API_KEY=
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Qdrant는 role로 admin/조회 인스턴스를 나눠 생성할 수 있습니다:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from echoss_db.qdrant_vector import QdrantVector
|
|
111
|
+
|
|
112
|
+
admin = QdrantVector("config/config.yaml") # role 기본값 "admin" -> qdrant.api_key 사용
|
|
113
|
+
readonly = QdrantVector("config/config.yaml", role="readonly") # qdrant.read_only_api_key 사용
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`api_key`/`read_only_api_key`를 둘 다 설정하지 않으면 기존과 동일하게 무인증으로 연결됩니다.
|
|
117
|
+
|
|
86
118
|
## Installation
|
|
87
119
|
|
|
88
120
|
---
|
|
@@ -106,6 +138,9 @@ Python 3.11 or lower is not supported in `echoss-db>=2.2.0`; use the `1.x` line
|
|
|
106
138
|
pip install -U echoss-db
|
|
107
139
|
```
|
|
108
140
|
|
|
141
|
+
`echoss-db>=2.3.0` requires pandas `>=3.0`. Environments pinned to pandas 2 (e.g. AutoGluon, PyCaret 3.x) should pin `"echoss-db<2.3"`.
|
|
142
|
+
`echoss-db>=2.3.0` no longer installs `echoss-fileformat`; declare it directly if your code imports it.
|
|
143
|
+
|
|
109
144
|
If your environment previously used legacy PostgreSQL drivers, clean them first:
|
|
110
145
|
|
|
111
146
|
```bash
|
|
@@ -596,3 +631,5 @@ v1.2.3 add postgres DB and qdrant vector storage support
|
|
|
596
631
|
v2.0.0 add optional `echoss_db.mapping` (dataclass default, pydantic optional)
|
|
597
632
|
v2.1.0 raise Python baseline to 3.11, standardize on psycopg3, and keep Qdrant fastembed as default runtime
|
|
598
633
|
v2.2.0 qdrant generalized dict filter (breaking behavior: previously ignored keys now applied) and get_collection_info, postgres upsert/parse_json options, upsert retry/backoff/batch_size, deprecate MongoQuery (pymongo moved to optional extra [mongo]), raise Python floor to 3.12
|
|
634
|
+
v2.2.2 qdrant admin/read-only API key support (`role` param, `api_key`/`read_only_api_key` config fields), config.yaml credential fields (mysql/postgres/elastic `passwd`, qdrant `api_key`/`read_only_api_key`) now accept `${VAR_NAME}` placeholders resolved from `.env`/OS env (new `python-dotenv` dependency)
|
|
635
|
+
v2.3.0 config loading and logger now imported from `echoss-common` (`>=1.2.0,<3`) instead of `echoss-fileformat`; `echoss-fileformat` removed from install dependencies (declare it directly if you use it); raise pandas floor to 3.0. XML config files are now read by `echoss-common` (result keys may differ from the previous `echoss-fileformat` XML reader). With `echoss-common` 2.0: no default `logs/echoss.log` file (console only), and a missing config file path raises `FileNotFoundError` (previously `ValueError`/`TypeError`)
|
|
@@ -5,16 +5,24 @@ MySQL, PostgreSQL, OpenSearch, Qdrant compatible query/vector access package
|
|
|
5
5
|
> ⚠️ **MongoDB support is deprecated** (v2.2.0) and will be removed in v3.0.0.
|
|
6
6
|
> `pymongo` is now an optional dependency: `pip install echoss-db[mongo]`.
|
|
7
7
|
|
|
8
|
+
> 작업 이력(완료된 결론·미착수 백로그)은 [`prior-work.jsonl`](./prior-work.jsonl)에 기록됩니다.
|
|
9
|
+
> 조회: `/prior-work <주제어>` (예: `qdrant`, `.env`, `mysql passwd`).
|
|
10
|
+
|
|
8
11
|
## Prepare
|
|
9
12
|
|
|
10
13
|
사용 전 config(인증 정보) 유무를 확인한 뒤 사용해야 합니다.
|
|
11
14
|
`config/config.example.yaml`을 `config/config.yaml`로 복사해 실제 값을 채우세요 (실 config는 gitignore 대상).
|
|
12
15
|
`config/config.yaml` 기준의 credential 제거 예시는 아래와 같습니다.
|
|
13
16
|
|
|
17
|
+
credential 필드(`passwd`, `api_key`, `read_only_api_key`)는 평문 대신 `${VAR_NAME}` 형태로
|
|
18
|
+
적으면 `.env`(또는 OS 환경변수)에서 값을 읽어 채웁니다. `.env`는 git에 추적되지 않으므로
|
|
19
|
+
`.env.example`을 `.env`로 복사해 실제 값을 채우세요. host/port 등 credential이 아닌 필드는
|
|
20
|
+
지금처럼 평문 값을 직접 씁니다.
|
|
21
|
+
|
|
14
22
|
```yaml
|
|
15
23
|
mysql:
|
|
16
24
|
user: <MYSQL_USER>
|
|
17
|
-
passwd:
|
|
25
|
+
passwd: ${MYSQL_PASSWD}
|
|
18
26
|
host: <MYSQL_HOST>
|
|
19
27
|
port: <MYSQL_PORT>
|
|
20
28
|
db: <MYSQL_DB>
|
|
@@ -27,7 +35,7 @@ mongo:
|
|
|
27
35
|
|
|
28
36
|
elastic:
|
|
29
37
|
user: <ELASTIC_USER>
|
|
30
|
-
passwd:
|
|
38
|
+
passwd: ${ELASTIC_PASSWD}
|
|
31
39
|
host: <ELASTIC_HOST>
|
|
32
40
|
port: <ELASTIC_PORT>
|
|
33
41
|
scheme: <http|https>
|
|
@@ -35,7 +43,7 @@ elastic:
|
|
|
35
43
|
|
|
36
44
|
postgres:
|
|
37
45
|
user: <POSTGRES_USER>
|
|
38
|
-
passwd:
|
|
46
|
+
passwd: ${POSTGRES_PASSWD}
|
|
39
47
|
host: <POSTGRES_HOST>
|
|
40
48
|
port: <POSTGRES_PORT>
|
|
41
49
|
db: <POSTGRES_DB>
|
|
@@ -50,8 +58,31 @@ qdrant:
|
|
|
50
58
|
default_limit: <DEFAULT_LIMIT>
|
|
51
59
|
# fastembed_model omitted -> default FastEmbed model automatically applied
|
|
52
60
|
# fastembed_model: null -> external vector mode (no FastEmbed)
|
|
61
|
+
api_key: ${QDRANT_API_KEY} # optional, admin/전체 권한
|
|
62
|
+
read_only_api_key: ${QDRANT_READ_ONLY_API_KEY} # optional, 조회 전용
|
|
53
63
|
```
|
|
54
64
|
|
|
65
|
+
`.env` 예시(`.env.example` 참고, 실값은 커밋하지 않음):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
MYSQL_PASSWD=
|
|
69
|
+
ELASTIC_PASSWD=
|
|
70
|
+
POSTGRES_PASSWD=
|
|
71
|
+
QDRANT_API_KEY=
|
|
72
|
+
QDRANT_READ_ONLY_API_KEY=
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Qdrant는 role로 admin/조회 인스턴스를 나눠 생성할 수 있습니다:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from echoss_db.qdrant_vector import QdrantVector
|
|
79
|
+
|
|
80
|
+
admin = QdrantVector("config/config.yaml") # role 기본값 "admin" -> qdrant.api_key 사용
|
|
81
|
+
readonly = QdrantVector("config/config.yaml", role="readonly") # qdrant.read_only_api_key 사용
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`api_key`/`read_only_api_key`를 둘 다 설정하지 않으면 기존과 동일하게 무인증으로 연결됩니다.
|
|
85
|
+
|
|
55
86
|
## Installation
|
|
56
87
|
|
|
57
88
|
---
|
|
@@ -75,6 +106,9 @@ Python 3.11 or lower is not supported in `echoss-db>=2.2.0`; use the `1.x` line
|
|
|
75
106
|
pip install -U echoss-db
|
|
76
107
|
```
|
|
77
108
|
|
|
109
|
+
`echoss-db>=2.3.0` requires pandas `>=3.0`. Environments pinned to pandas 2 (e.g. AutoGluon, PyCaret 3.x) should pin `"echoss-db<2.3"`.
|
|
110
|
+
`echoss-db>=2.3.0` no longer installs `echoss-fileformat`; declare it directly if your code imports it.
|
|
111
|
+
|
|
78
112
|
If your environment previously used legacy PostgreSQL drivers, clean them first:
|
|
79
113
|
|
|
80
114
|
```bash
|
|
@@ -565,3 +599,5 @@ v1.2.3 add postgres DB and qdrant vector storage support
|
|
|
565
599
|
v2.0.0 add optional `echoss_db.mapping` (dataclass default, pydantic optional)
|
|
566
600
|
v2.1.0 raise Python baseline to 3.11, standardize on psycopg3, and keep Qdrant fastembed as default runtime
|
|
567
601
|
v2.2.0 qdrant generalized dict filter (breaking behavior: previously ignored keys now applied) and get_collection_info, postgres upsert/parse_json options, upsert retry/backoff/batch_size, deprecate MongoQuery (pymongo moved to optional extra [mongo]), raise Python floor to 3.12
|
|
602
|
+
v2.2.2 qdrant admin/read-only API key support (`role` param, `api_key`/`read_only_api_key` config fields), config.yaml credential fields (mysql/postgres/elastic `passwd`, qdrant `api_key`/`read_only_api_key`) now accept `${VAR_NAME}` placeholders resolved from `.env`/OS env (new `python-dotenv` dependency)
|
|
603
|
+
v2.3.0 config loading and logger now imported from `echoss-common` (`>=1.2.0,<3`) instead of `echoss-fileformat`; `echoss-fileformat` removed from install dependencies (declare it directly if you use it); raise pandas floor to 3.0. XML config files are now read by `echoss-common` (result keys may differ from the previous `echoss-fileformat` XML reader). With `echoss-common` 2.0: no default `logs/echoss.log` file (console only), and a missing config file path raises `FileNotFoundError` (previously `ValueError`/`TypeError`)
|
|
@@ -4,7 +4,9 @@ from opensearchpy.helpers import bulk as helpers_bulk
|
|
|
4
4
|
from opensearchpy.exceptions import NotFoundError
|
|
5
5
|
from typing import Any, List, Tuple, Dict, Optional, Union
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from echoss_common import dict_load, get_logger, set_logger_level
|
|
8
|
+
|
|
9
|
+
from .env_config import resolve_credential
|
|
8
10
|
|
|
9
11
|
logger = get_logger("echoss_query")
|
|
10
12
|
|
|
@@ -33,7 +35,7 @@ class ElasticSearch:
|
|
|
33
35
|
|
|
34
36
|
"""
|
|
35
37
|
if isinstance(conn_info, str):
|
|
36
|
-
conn_info =
|
|
38
|
+
conn_info = dict_load(conn_info)
|
|
37
39
|
elif not isinstance(conn_info, dict):
|
|
38
40
|
raise TypeError("ElasticSearch support type 'str' and 'dict'")
|
|
39
41
|
|
|
@@ -44,7 +46,7 @@ class ElasticSearch:
|
|
|
44
46
|
raise TypeError("[Elastic] config info not exist")
|
|
45
47
|
|
|
46
48
|
self.user = es_config.get('user')
|
|
47
|
-
self.passwd = es_config.get('passwd')
|
|
49
|
+
self.passwd = resolve_credential(es_config.get('passwd'), section='elastic', field='passwd')
|
|
48
50
|
self.auth = (self.user, self.passwd) if self.user and self.passwd else None
|
|
49
51
|
|
|
50
52
|
self.host = es_config['host']
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""PLACEHOLDER 치환 공용 헬퍼.
|
|
2
|
+
|
|
3
|
+
Design Ref: §4.1, §9 — mysql/postgres/elastic/qdrant 4개 DB 모듈이 공통으로 import하는
|
|
4
|
+
credential 치환 로직. config.yaml의 값이 '${VAR_NAME}' 전체 패턴과 일치하면 .env/OS
|
|
5
|
+
환경변수에서 VAR_NAME을 찾아 치환하고, 아니면 원본 값을 그대로 반환한다(평문 하위 호환).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from dotenv import find_dotenv, load_dotenv
|
|
14
|
+
|
|
15
|
+
PLACEHOLDER_PATTERN = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
16
|
+
|
|
17
|
+
_dotenv_loaded = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _ensure_dotenv_loaded() -> None:
|
|
21
|
+
# Design Ref: §4.1 — 프로세스당 1회만 로드, OS 환경변수가 .env보다 우선(override=False, FR-04).
|
|
22
|
+
global _dotenv_loaded
|
|
23
|
+
if _dotenv_loaded:
|
|
24
|
+
return
|
|
25
|
+
load_dotenv(find_dotenv(usecwd=True), override=False)
|
|
26
|
+
_dotenv_loaded = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_credential(value: Optional[str], *, section: str, field: str) -> Optional[str]:
|
|
30
|
+
"""value가 '${VAR_NAME}' 전체 일치 패턴이면 .env/OS 환경변수에서 VAR_NAME을 찾아 반환한다.
|
|
31
|
+
|
|
32
|
+
패턴이 아니면 value를 그대로 반환한다(평문 config 하위 호환). value가 None이거나
|
|
33
|
+
문자열이 아니면 그대로 반환한다.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: PLACEHOLDER이지만 .env/OS 어디에도 VAR_NAME이 없을 때(fail-closed, FR-06).
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(value, str):
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
match = PLACEHOLDER_PATTERN.match(value)
|
|
42
|
+
if not match:
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
_ensure_dotenv_loaded()
|
|
46
|
+
var_name = match.group(1)
|
|
47
|
+
resolved = os.environ.get(var_name)
|
|
48
|
+
if resolved is None:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"[{section}.{field}] environment variable '{var_name}' not found "
|
|
51
|
+
f"(.env / OS env). Set it before creating this connection."
|
|
52
|
+
)
|
|
53
|
+
return resolved
|
|
@@ -5,7 +5,7 @@ from pymongo import MongoClient
|
|
|
5
5
|
from pymongo.errors import PyMongoError
|
|
6
6
|
from typing import Any, List, Tuple, Union
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from echoss_common import dict_load, get_logger, set_logger_level
|
|
9
9
|
|
|
10
10
|
logger = get_logger("echoss_query")
|
|
11
11
|
|
|
@@ -34,7 +34,7 @@ class MongoQuery:
|
|
|
34
34
|
stacklevel=2,
|
|
35
35
|
)
|
|
36
36
|
if isinstance(conn_info, str):
|
|
37
|
-
conn_info =
|
|
37
|
+
conn_info = dict_load(conn_info)
|
|
38
38
|
elif not isinstance(conn_info, dict):
|
|
39
39
|
raise TypeError("[Mongo] support type 'str' and 'dict'")
|
|
40
40
|
required_keys = ['db']
|
|
@@ -9,7 +9,8 @@ from sqlalchemy import create_engine, text
|
|
|
9
9
|
from sqlalchemy.engine import Engine, Connection, Result
|
|
10
10
|
from sqlalchemy.exc import SQLAlchemyError, DBAPIError
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from echoss_common import dict_load, get_logger
|
|
13
|
+
from .env_config import resolve_credential
|
|
13
14
|
from .sql_transaction import SQLTransaction
|
|
14
15
|
|
|
15
16
|
logger = get_logger("echoss_db")
|
|
@@ -130,7 +131,7 @@ class MysqlQuery:
|
|
|
130
131
|
use_percent_param: use %s param style, if false sqlalchemy :name tag style query param
|
|
131
132
|
"""
|
|
132
133
|
if isinstance(conn_info, str):
|
|
133
|
-
conn_info =
|
|
134
|
+
conn_info = dict_load(conn_info)
|
|
134
135
|
elif not isinstance(conn_info, dict):
|
|
135
136
|
raise TypeError("MysqlQuery support type 'str' and 'dict'")
|
|
136
137
|
|
|
@@ -143,7 +144,7 @@ class MysqlQuery:
|
|
|
143
144
|
if (len(conn_info) > 0) and ('mysql' in conn_info) and all(key in conn_info['mysql'] for key in required_keys):
|
|
144
145
|
m = conn_info["mysql"]
|
|
145
146
|
self.user = m['user']
|
|
146
|
-
self.passwd = m['passwd']
|
|
147
|
+
self.passwd = resolve_credential(m['passwd'], section='mysql', field='passwd')
|
|
147
148
|
self.host = m['host']
|
|
148
149
|
self.port = m.get('port', 3306)
|
|
149
150
|
self.db = m['db']
|
|
@@ -10,7 +10,8 @@ from sqlalchemy import create_engine, text
|
|
|
10
10
|
from sqlalchemy.engine import Engine, Connection, Result
|
|
11
11
|
from sqlalchemy.exc import SQLAlchemyError
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from echoss_common import dict_load, get_logger
|
|
14
|
+
from .env_config import resolve_credential
|
|
14
15
|
from .sql_transaction import SQLTransaction
|
|
15
16
|
|
|
16
17
|
logger = get_logger("echoss_db")
|
|
@@ -94,7 +95,7 @@ class PostgresQuery:
|
|
|
94
95
|
pool_size: int = 5, pool_timeout: int = 30, pool_recycle: Optional[int] = None,
|
|
95
96
|
use_percent_param: bool = True):
|
|
96
97
|
if isinstance(conn_info, str):
|
|
97
|
-
conn_info =
|
|
98
|
+
conn_info = dict_load(conn_info)
|
|
98
99
|
elif not isinstance(conn_info, dict):
|
|
99
100
|
raise TypeError("PostgresQuery support type 'str' and 'dict'")
|
|
100
101
|
|
|
@@ -107,7 +108,7 @@ class PostgresQuery:
|
|
|
107
108
|
if (len(conn_info) > 0) and ('postgres' in conn_info) and all(k in conn_info['postgres'] for k in required_keys):
|
|
108
109
|
p = conn_info["postgres"]
|
|
109
110
|
self.user = p['user']
|
|
110
|
-
self.passwd = p['passwd']
|
|
111
|
+
self.passwd = resolve_credential(p['passwd'], section='postgres', field='passwd')
|
|
111
112
|
self.host = p['host']
|
|
112
113
|
self.port = p.get('port', 5432)
|
|
113
114
|
self.db = p['db']
|
|
@@ -3,13 +3,15 @@ import time
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from typing import Any, Dict, List, Optional, Union
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from echoss_common import dict_load, get_logger
|
|
7
7
|
|
|
8
8
|
logger = get_logger("echoss_query")
|
|
9
9
|
|
|
10
10
|
from qdrant_client import QdrantClient
|
|
11
11
|
from qdrant_client.http import models as qm
|
|
12
12
|
|
|
13
|
+
from .env_config import resolve_credential
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
class QdrantVector:
|
|
15
17
|
conn = None
|
|
@@ -17,7 +19,7 @@ class QdrantVector:
|
|
|
17
19
|
default_limit = 10
|
|
18
20
|
default_fastembed_model = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
|
19
21
|
|
|
20
|
-
def __init__(self, conn_info: Union[str, dict]):
|
|
22
|
+
def __init__(self, conn_info: Union[str, dict], role: str = "admin"):
|
|
21
23
|
"""
|
|
22
24
|
Args:
|
|
23
25
|
conn_info:
|
|
@@ -28,12 +30,16 @@ class QdrantVector:
|
|
|
28
30
|
'scheme': 'http',
|
|
29
31
|
'collection': 'ai_rag_chunks',
|
|
30
32
|
'timeout': 5,
|
|
31
|
-
'default_limit': 10
|
|
33
|
+
'default_limit': 10,
|
|
34
|
+
'api_key': '${QDRANT_API_KEY}', # admin, optional
|
|
35
|
+
'read_only_api_key': '${QDRANT_READ_ONLY_API_KEY}', # readonly, optional
|
|
32
36
|
}
|
|
33
37
|
}
|
|
38
|
+
role: "admin"(기본값) 또는 "readonly". config의 qdrant.api_key/read_only_api_key
|
|
39
|
+
중 role에 대응하는 필드를 연결에 사용한다. Design Ref: §4.2.
|
|
34
40
|
"""
|
|
35
41
|
if isinstance(conn_info, str):
|
|
36
|
-
conn_info =
|
|
42
|
+
conn_info = dict_load(conn_info)
|
|
37
43
|
elif not isinstance(conn_info, dict):
|
|
38
44
|
raise TypeError("QdrantVector support type 'str' and 'dict'")
|
|
39
45
|
|
|
@@ -43,6 +49,23 @@ class QdrantVector:
|
|
|
43
49
|
else:
|
|
44
50
|
raise TypeError("[Qdrant] config info not exist")
|
|
45
51
|
|
|
52
|
+
if role not in ("admin", "readonly"):
|
|
53
|
+
raise ValueError(f"QdrantVector role must be 'admin' or 'readonly', got '{role}'")
|
|
54
|
+
self.role = role
|
|
55
|
+
|
|
56
|
+
# Design Ref: §4.2 — role에 대응하는 키 필드가 없고 다른 role의 키만 있으면 에러(FR-03).
|
|
57
|
+
# 둘 다 없으면 기존과 동일하게 무인증 연결(하위 호환).
|
|
58
|
+
key_field = "api_key" if role == "admin" else "read_only_api_key"
|
|
59
|
+
other_field = "read_only_api_key" if role == "admin" else "api_key"
|
|
60
|
+
if key_field in q_config:
|
|
61
|
+
self.api_key = resolve_credential(q_config[key_field], section="qdrant", field=key_field)
|
|
62
|
+
elif other_field in q_config:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"role='{role}' requires qdrant.{key_field}, but only qdrant.{other_field} is configured"
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
self.api_key = None
|
|
68
|
+
|
|
46
69
|
self.host = q_config['host']
|
|
47
70
|
self.port = q_config.get('port', 6333)
|
|
48
71
|
self.scheme = q_config.get('scheme', 'http')
|
|
@@ -82,7 +105,7 @@ class QdrantVector:
|
|
|
82
105
|
|
|
83
106
|
def connect(self):
|
|
84
107
|
try:
|
|
85
|
-
cli = QdrantClient(url=self.url, timeout=self.timeout)
|
|
108
|
+
cli = QdrantClient(url=self.url, api_key=self.api_key, timeout=self.timeout)
|
|
86
109
|
# 간단 ping: collections 호출
|
|
87
110
|
cli.get_collections()
|
|
88
111
|
return cli
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: echoss-db
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: echoss AI Bigdata Solution - Database Query Package
|
|
5
5
|
Author-email: ckkim <ckkim@12cm.co.kr>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -13,13 +13,14 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: pandas>=
|
|
16
|
+
Requires-Dist: pandas>=3.0
|
|
17
17
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
18
18
|
Requires-Dist: PyMySQL>=1.0.2
|
|
19
19
|
Requires-Dist: opensearch-py<3.0.0,>=2.8.0
|
|
20
|
-
Requires-Dist: echoss-
|
|
20
|
+
Requires-Dist: echoss-common<3,>=1.2.0
|
|
21
21
|
Requires-Dist: psycopg[binary]<4.0.0,>=3.2
|
|
22
22
|
Requires-Dist: qdrant-client[fastembed]<2.0.0,>=1.14.1
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
23
24
|
Provides-Extra: mapping
|
|
24
25
|
Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "mapping"
|
|
25
26
|
Provides-Extra: mongo
|
|
@@ -36,16 +37,24 @@ MySQL, PostgreSQL, OpenSearch, Qdrant compatible query/vector access package
|
|
|
36
37
|
> ⚠️ **MongoDB support is deprecated** (v2.2.0) and will be removed in v3.0.0.
|
|
37
38
|
> `pymongo` is now an optional dependency: `pip install echoss-db[mongo]`.
|
|
38
39
|
|
|
40
|
+
> 작업 이력(완료된 결론·미착수 백로그)은 [`prior-work.jsonl`](./prior-work.jsonl)에 기록됩니다.
|
|
41
|
+
> 조회: `/prior-work <주제어>` (예: `qdrant`, `.env`, `mysql passwd`).
|
|
42
|
+
|
|
39
43
|
## Prepare
|
|
40
44
|
|
|
41
45
|
사용 전 config(인증 정보) 유무를 확인한 뒤 사용해야 합니다.
|
|
42
46
|
`config/config.example.yaml`을 `config/config.yaml`로 복사해 실제 값을 채우세요 (실 config는 gitignore 대상).
|
|
43
47
|
`config/config.yaml` 기준의 credential 제거 예시는 아래와 같습니다.
|
|
44
48
|
|
|
49
|
+
credential 필드(`passwd`, `api_key`, `read_only_api_key`)는 평문 대신 `${VAR_NAME}` 형태로
|
|
50
|
+
적으면 `.env`(또는 OS 환경변수)에서 값을 읽어 채웁니다. `.env`는 git에 추적되지 않으므로
|
|
51
|
+
`.env.example`을 `.env`로 복사해 실제 값을 채우세요. host/port 등 credential이 아닌 필드는
|
|
52
|
+
지금처럼 평문 값을 직접 씁니다.
|
|
53
|
+
|
|
45
54
|
```yaml
|
|
46
55
|
mysql:
|
|
47
56
|
user: <MYSQL_USER>
|
|
48
|
-
passwd:
|
|
57
|
+
passwd: ${MYSQL_PASSWD}
|
|
49
58
|
host: <MYSQL_HOST>
|
|
50
59
|
port: <MYSQL_PORT>
|
|
51
60
|
db: <MYSQL_DB>
|
|
@@ -58,7 +67,7 @@ mongo:
|
|
|
58
67
|
|
|
59
68
|
elastic:
|
|
60
69
|
user: <ELASTIC_USER>
|
|
61
|
-
passwd:
|
|
70
|
+
passwd: ${ELASTIC_PASSWD}
|
|
62
71
|
host: <ELASTIC_HOST>
|
|
63
72
|
port: <ELASTIC_PORT>
|
|
64
73
|
scheme: <http|https>
|
|
@@ -66,7 +75,7 @@ elastic:
|
|
|
66
75
|
|
|
67
76
|
postgres:
|
|
68
77
|
user: <POSTGRES_USER>
|
|
69
|
-
passwd:
|
|
78
|
+
passwd: ${POSTGRES_PASSWD}
|
|
70
79
|
host: <POSTGRES_HOST>
|
|
71
80
|
port: <POSTGRES_PORT>
|
|
72
81
|
db: <POSTGRES_DB>
|
|
@@ -81,8 +90,31 @@ qdrant:
|
|
|
81
90
|
default_limit: <DEFAULT_LIMIT>
|
|
82
91
|
# fastembed_model omitted -> default FastEmbed model automatically applied
|
|
83
92
|
# fastembed_model: null -> external vector mode (no FastEmbed)
|
|
93
|
+
api_key: ${QDRANT_API_KEY} # optional, admin/전체 권한
|
|
94
|
+
read_only_api_key: ${QDRANT_READ_ONLY_API_KEY} # optional, 조회 전용
|
|
84
95
|
```
|
|
85
96
|
|
|
97
|
+
`.env` 예시(`.env.example` 참고, 실값은 커밋하지 않음):
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
MYSQL_PASSWD=
|
|
101
|
+
ELASTIC_PASSWD=
|
|
102
|
+
POSTGRES_PASSWD=
|
|
103
|
+
QDRANT_API_KEY=
|
|
104
|
+
QDRANT_READ_ONLY_API_KEY=
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Qdrant는 role로 admin/조회 인스턴스를 나눠 생성할 수 있습니다:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from echoss_db.qdrant_vector import QdrantVector
|
|
111
|
+
|
|
112
|
+
admin = QdrantVector("config/config.yaml") # role 기본값 "admin" -> qdrant.api_key 사용
|
|
113
|
+
readonly = QdrantVector("config/config.yaml", role="readonly") # qdrant.read_only_api_key 사용
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`api_key`/`read_only_api_key`를 둘 다 설정하지 않으면 기존과 동일하게 무인증으로 연결됩니다.
|
|
117
|
+
|
|
86
118
|
## Installation
|
|
87
119
|
|
|
88
120
|
---
|
|
@@ -106,6 +138,9 @@ Python 3.11 or lower is not supported in `echoss-db>=2.2.0`; use the `1.x` line
|
|
|
106
138
|
pip install -U echoss-db
|
|
107
139
|
```
|
|
108
140
|
|
|
141
|
+
`echoss-db>=2.3.0` requires pandas `>=3.0`. Environments pinned to pandas 2 (e.g. AutoGluon, PyCaret 3.x) should pin `"echoss-db<2.3"`.
|
|
142
|
+
`echoss-db>=2.3.0` no longer installs `echoss-fileformat`; declare it directly if your code imports it.
|
|
143
|
+
|
|
109
144
|
If your environment previously used legacy PostgreSQL drivers, clean them first:
|
|
110
145
|
|
|
111
146
|
```bash
|
|
@@ -596,3 +631,5 @@ v1.2.3 add postgres DB and qdrant vector storage support
|
|
|
596
631
|
v2.0.0 add optional `echoss_db.mapping` (dataclass default, pydantic optional)
|
|
597
632
|
v2.1.0 raise Python baseline to 3.11, standardize on psycopg3, and keep Qdrant fastembed as default runtime
|
|
598
633
|
v2.2.0 qdrant generalized dict filter (breaking behavior: previously ignored keys now applied) and get_collection_info, postgres upsert/parse_json options, upsert retry/backoff/batch_size, deprecate MongoQuery (pymongo moved to optional extra [mongo]), raise Python floor to 3.12
|
|
634
|
+
v2.2.2 qdrant admin/read-only API key support (`role` param, `api_key`/`read_only_api_key` config fields), config.yaml credential fields (mysql/postgres/elastic `passwd`, qdrant `api_key`/`read_only_api_key`) now accept `${VAR_NAME}` placeholders resolved from `.env`/OS env (new `python-dotenv` dependency)
|
|
635
|
+
v2.3.0 config loading and logger now imported from `echoss-common` (`>=1.2.0,<3`) instead of `echoss-fileformat`; `echoss-fileformat` removed from install dependencies (declare it directly if you use it); raise pandas floor to 3.0. XML config files are now read by `echoss-common` (result keys may differ from the previous `echoss-fileformat` XML reader). With `echoss-common` 2.0: no default `logs/echoss.log` file (console only), and a missing config file path raises `FileNotFoundError` (previously `ValueError`/`TypeError`)
|
|
@@ -5,6 +5,7 @@ pyproject.toml
|
|
|
5
5
|
requirements.txt
|
|
6
6
|
echoss_db/__init__.py
|
|
7
7
|
echoss_db/elastic_search.py
|
|
8
|
+
echoss_db/env_config.py
|
|
8
9
|
echoss_db/mongo_query.py
|
|
9
10
|
echoss_db/mysql_query.py
|
|
10
11
|
echoss_db/postgres_query.py
|
|
@@ -26,6 +27,7 @@ package_tests/test_package_imports.py
|
|
|
26
27
|
tests/__init__.py
|
|
27
28
|
tests/integration/__init__.py
|
|
28
29
|
tests/integration/conftest.py
|
|
30
|
+
tests/integration/test_env_config_integration.py
|
|
29
31
|
tests/integration/test_postgres_integration.py
|
|
30
32
|
tests/integration/test_qdrant_integration.py
|
|
31
33
|
tests/manual/__init__.py
|
|
@@ -42,5 +44,7 @@ tests/manual/example_postgres_qdrant_ingest.py
|
|
|
42
44
|
tests/manual/example_qdrant.py
|
|
43
45
|
tests/manual/setup_ai_rag_test_schema.sql
|
|
44
46
|
tests/unit/__init__.py
|
|
47
|
+
tests/unit/test_env_config.py
|
|
48
|
+
tests/unit/test_qdrant_role.py
|
|
45
49
|
tests/unit/mapping/__init__.py
|
|
46
50
|
tests/unit/mapping/test_param_mapper.py
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
pandas>=
|
|
1
|
+
pandas>=3.0
|
|
2
2
|
sqlalchemy>=2.0.0
|
|
3
3
|
PyMySQL>=1.0.2
|
|
4
4
|
opensearch-py<3.0.0,>=2.8.0
|
|
5
|
-
echoss-
|
|
5
|
+
echoss-common<3,>=1.2.0
|
|
6
6
|
psycopg[binary]<4.0.0,>=3.2
|
|
7
7
|
qdrant-client[fastembed]<2.0.0,>=1.14.1
|
|
8
|
+
python-dotenv>=1.0.0
|
|
8
9
|
|
|
9
10
|
[all]
|
|
10
11
|
pydantic<3.0.0,>=2.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "echoss-db"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "echoss AI Bigdata Solution - Database Query Package"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -20,13 +20,14 @@ classifiers = [
|
|
|
20
20
|
"Operating System :: OS Independent",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"pandas>=
|
|
23
|
+
"pandas>=3.0",
|
|
24
24
|
"sqlalchemy>=2.0.0",
|
|
25
25
|
"PyMySQL>=1.0.2",
|
|
26
26
|
"opensearch-py>=2.8.0,<3.0.0",
|
|
27
|
-
"echoss-
|
|
27
|
+
"echoss-common>=1.2.0,<3",
|
|
28
28
|
"psycopg[binary]>=3.2,<4.0.0",
|
|
29
29
|
"qdrant-client[fastembed]>=1.14.1,<2.0.0",
|
|
30
|
+
"python-dotenv>=1.0.0",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[project.optional-dependencies]
|
|
@@ -15,7 +15,7 @@ import os
|
|
|
15
15
|
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
|
-
from
|
|
18
|
+
from echoss_common import dict_load
|
|
19
19
|
|
|
20
20
|
from echoss_db.postgres_query import PostgresQuery
|
|
21
21
|
from echoss_db.qdrant_vector import QdrantVector
|
|
@@ -23,14 +23,14 @@ from echoss_db.qdrant_vector import QdrantVector
|
|
|
23
23
|
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
24
24
|
CONFIG_PATH = os.path.join(PROJECT_ROOT, "config", "config_dev.yaml")
|
|
25
25
|
|
|
26
|
-
POSTGRES_TEST_SCHEMA = "
|
|
27
|
-
QDRANT_TEST_COLLECTION = "
|
|
26
|
+
POSTGRES_TEST_SCHEMA = "ai_push_rag_test"
|
|
27
|
+
QDRANT_TEST_COLLECTION = "ai_push_chunks_test"
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def load_config_section(section: str) -> dict:
|
|
31
31
|
if not os.path.exists(CONFIG_PATH):
|
|
32
32
|
pytest.skip(f"config/config_dev.yaml not found — required for {section} integration tests")
|
|
33
|
-
config =
|
|
33
|
+
config = dict_load(CONFIG_PATH)
|
|
34
34
|
if section not in config:
|
|
35
35
|
pytest.skip(f"'{section}' section missing in config/config_dev.yaml")
|
|
36
36
|
return dict(config[section])
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Real Postgres integration tests for env_config PLACEHOLDER credential resolution.
|
|
2
|
+
|
|
3
|
+
Verifies that a config.yaml credential field using '${VAR_NAME}' connects exactly like a
|
|
4
|
+
plaintext credential, using the real config_dev.yaml postgres section as the source of
|
|
5
|
+
truth for actual credentials (no fake DB, per [[no-fake-db-tests]]).
|
|
6
|
+
Run: pytest -m integration tests/integration/test_env_config_integration.py
|
|
7
|
+
|
|
8
|
+
Design Ref: §8.4. Both tests run an actual SELECT — PostgresQuery.connect_db() only
|
|
9
|
+
builds a lazy SQLAlchemy Engine and does not validate credentials at construction time,
|
|
10
|
+
so asserting on `client.engine` alone would not prove a real, correctly-authenticated
|
|
11
|
+
connection.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from echoss_db.env_config import resolve_credential
|
|
18
|
+
from echoss_db.postgres_query import PostgresQuery
|
|
19
|
+
|
|
20
|
+
from .conftest import POSTGRES_TEST_SCHEMA, load_config_section
|
|
21
|
+
|
|
22
|
+
pytestmark = pytest.mark.integration
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_postgres_connects_with_placeholder_passwd():
|
|
26
|
+
# config_dev.yaml의 postgres.passwd는 실제로 "${AI_PUSH_PG_PASSWD}" PLACEHOLDER다.
|
|
27
|
+
# .env/OS env에 실값이 없으면 이 시나리오 자체를 검증할 수 없으므로 skip한다.
|
|
28
|
+
pg_cfg = dict(load_config_section("postgres"))
|
|
29
|
+
pg_cfg["schema"] = POSTGRES_TEST_SCHEMA
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
client = PostgresQuery({"postgres": pg_cfg})
|
|
33
|
+
except ValueError as e:
|
|
34
|
+
pytest.skip(f"PLACEHOLDER env var not set — cannot verify: {e}")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
df = client.select("SELECT 1 AS ok")
|
|
38
|
+
assert df.loc[0, "ok"] == 1
|
|
39
|
+
finally:
|
|
40
|
+
client.close()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_postgres_still_connects_with_plaintext_passwd():
|
|
44
|
+
# 기존 평문 config 하위 호환 회귀 확인 (FR-07) — 실 패스워드를 평문으로 config에 직접
|
|
45
|
+
# 넣어도 동작해야 한다. config_dev.yaml 자체는 이제 PLACEHOLDER를 쓰므로, 진짜 비밀값은
|
|
46
|
+
# resolve_credential로 한 번 해석해서(값을 출력/로그에 남기지 않고) 평문으로 재사용한다.
|
|
47
|
+
pg_cfg = dict(load_config_section("postgres"))
|
|
48
|
+
try:
|
|
49
|
+
real_passwd = resolve_credential(pg_cfg["passwd"], section="postgres", field="passwd")
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
pytest.skip(f"cannot resolve real postgres passwd for plaintext regression test: {e}")
|
|
52
|
+
|
|
53
|
+
pg_cfg["passwd"] = real_passwd # 평문 — PLACEHOLDER 아님
|
|
54
|
+
pg_cfg["schema"] = POSTGRES_TEST_SCHEMA
|
|
55
|
+
|
|
56
|
+
client = PostgresQuery({"postgres": pg_cfg})
|
|
57
|
+
try:
|
|
58
|
+
df = client.select("SELECT 1 AS ok")
|
|
59
|
+
assert df.loc[0, "ok"] == 1
|
|
60
|
+
finally:
|
|
61
|
+
client.close()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_missing_placeholder_env_var_raises(monkeypatch):
|
|
65
|
+
pg_cfg = dict(load_config_section("postgres"))
|
|
66
|
+
monkeypatch.delenv("ECHOSS_IT_NONEXISTENT_VAR", raising=False)
|
|
67
|
+
pg_cfg["passwd"] = "${ECHOSS_IT_NONEXISTENT_VAR}"
|
|
68
|
+
pg_cfg["schema"] = POSTGRES_TEST_SCHEMA
|
|
69
|
+
|
|
70
|
+
with pytest.raises(ValueError, match="ECHOSS_IT_NONEXISTENT_VAR"):
|
|
71
|
+
PostgresQuery({"postgres": pg_cfg})
|
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
Exercises actual create_collection -> upsert -> get_collection_info -> search(filter)
|
|
4
4
|
-> delete against a live Qdrant in external (precomputed) vector mode.
|
|
5
5
|
Run: pytest -m integration tests/integration/test_qdrant_integration.py
|
|
6
|
+
|
|
7
|
+
Design Ref: §8.3 — role-based API key scenarios are skipped when config_dev.yaml's
|
|
8
|
+
qdrant section has no api_key/read_only_api_key (this dev instance has no auth enabled).
|
|
6
9
|
"""
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
9
12
|
import pytest
|
|
10
13
|
|
|
14
|
+
from echoss_db.qdrant_vector import QdrantVector
|
|
15
|
+
|
|
16
|
+
from .conftest import QDRANT_TEST_COLLECTION, load_config_section
|
|
17
|
+
|
|
11
18
|
pytestmark = pytest.mark.integration
|
|
12
19
|
|
|
13
20
|
DIM = 8
|
|
@@ -64,3 +71,59 @@ def test_search_passthrough_raw_filter(qv):
|
|
|
64
71
|
raw = qm.Filter(must_not=[qm.FieldCondition(key="doc_type", match=qm.MatchValue(value="b"))])
|
|
65
72
|
hits = qv.search_vector(_vec(0.1), filter_=raw, limit=10)
|
|
66
73
|
assert {h.id for h in hits} == {1}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _role_test_config():
|
|
77
|
+
q_cfg = dict(load_config_section("qdrant"))
|
|
78
|
+
if "api_key" not in q_cfg and "read_only_api_key" not in q_cfg:
|
|
79
|
+
pytest.skip(
|
|
80
|
+
"qdrant.api_key/read_only_api_key not configured in config_dev.yaml — "
|
|
81
|
+
"this dev Qdrant instance has no API key auth enabled"
|
|
82
|
+
)
|
|
83
|
+
q_cfg["collection"] = QDRANT_TEST_COLLECTION
|
|
84
|
+
q_cfg["fastembed_model"] = None
|
|
85
|
+
q_cfg.setdefault("timeout", 30)
|
|
86
|
+
return q_cfg
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_admin_role_connects_and_can_write():
|
|
90
|
+
q_cfg = _role_test_config()
|
|
91
|
+
client = QdrantVector({"qdrant": q_cfg}, role="admin")
|
|
92
|
+
try:
|
|
93
|
+
client.delete_collection()
|
|
94
|
+
client.create_collection(dim=DIM)
|
|
95
|
+
client.upsert_vectors([{"id": 1, "vector": _vec(0.1)}], wait=True)
|
|
96
|
+
assert client.get_collection_info()["points_count"] >= 1
|
|
97
|
+
finally:
|
|
98
|
+
try:
|
|
99
|
+
client.delete_collection()
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
client.close()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_readonly_role_connects_and_can_search_but_not_write():
|
|
106
|
+
q_cfg = _role_test_config()
|
|
107
|
+
if "read_only_api_key" not in q_cfg:
|
|
108
|
+
pytest.skip("qdrant.read_only_api_key not configured in config_dev.yaml")
|
|
109
|
+
|
|
110
|
+
admin = QdrantVector(dict({"qdrant": q_cfg}), role="admin")
|
|
111
|
+
try:
|
|
112
|
+
admin.delete_collection()
|
|
113
|
+
admin.create_collection(dim=DIM)
|
|
114
|
+
admin.upsert_vectors([{"id": 1, "vector": _vec(0.1)}], wait=True)
|
|
115
|
+
|
|
116
|
+
readonly = QdrantVector({"qdrant": q_cfg}, role="readonly")
|
|
117
|
+
try:
|
|
118
|
+
hits = readonly.search_vector(_vec(0.1), limit=10)
|
|
119
|
+
assert {h.id for h in hits} == {1}
|
|
120
|
+
with pytest.raises(Exception):
|
|
121
|
+
readonly.create_collection(dim=DIM, collection="echoss_it_readonly_should_fail")
|
|
122
|
+
finally:
|
|
123
|
+
readonly.close()
|
|
124
|
+
finally:
|
|
125
|
+
try:
|
|
126
|
+
admin.delete_collection()
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
admin.close()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Pure function tests for echoss_db.env_config.resolve_credential.
|
|
2
|
+
|
|
3
|
+
No DB involved — this is a pure function (PLACEHOLDER regex + env var lookup), so unit
|
|
4
|
+
testing it here is not a fake-DB test (see [[no-fake-db-tests]]).
|
|
5
|
+
Design Ref: §8.2.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from echoss_db import env_config
|
|
12
|
+
from echoss_db.env_config import resolve_credential
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def reset_dotenv_cache(monkeypatch):
|
|
17
|
+
# _dotenv_loaded is a process-wide singleton flag; reset per test for isolation.
|
|
18
|
+
monkeypatch.setattr(env_config, "_dotenv_loaded", False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_resolves_from_os_env(monkeypatch):
|
|
22
|
+
monkeypatch.setenv("MYSQL_PASSWD", "secret")
|
|
23
|
+
monkeypatch.setattr(env_config, "find_dotenv", lambda usecwd=True: "")
|
|
24
|
+
assert resolve_credential("${MYSQL_PASSWD}", section="mysql", field="passwd") == "secret"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_os_env_overrides_dotenv(monkeypatch, tmp_path):
|
|
28
|
+
dotenv_file = tmp_path / ".env"
|
|
29
|
+
dotenv_file.write_text("MYSQL_PASSWD=fromdotenv\n")
|
|
30
|
+
monkeypatch.setattr(env_config, "find_dotenv", lambda usecwd=True: str(dotenv_file))
|
|
31
|
+
monkeypatch.setenv("MYSQL_PASSWD", "fromos")
|
|
32
|
+
|
|
33
|
+
assert resolve_credential("${MYSQL_PASSWD}", section="mysql", field="passwd") == "fromos"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_loads_from_dotenv_when_os_env_absent(monkeypatch, tmp_path):
|
|
37
|
+
dotenv_file = tmp_path / ".env"
|
|
38
|
+
dotenv_file.write_text("MYSQL_PASSWD=fromdotenv\n")
|
|
39
|
+
monkeypatch.delenv("MYSQL_PASSWD", raising=False)
|
|
40
|
+
monkeypatch.setattr(env_config, "find_dotenv", lambda usecwd=True: str(dotenv_file))
|
|
41
|
+
|
|
42
|
+
assert resolve_credential("${MYSQL_PASSWD}", section="mysql", field="passwd") == "fromdotenv"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_missing_variable_raises(monkeypatch):
|
|
46
|
+
monkeypatch.delenv("MYSQL_PASSWD", raising=False)
|
|
47
|
+
monkeypatch.setattr(env_config, "find_dotenv", lambda usecwd=True: "")
|
|
48
|
+
|
|
49
|
+
with pytest.raises(ValueError, match="MYSQL_PASSWD"):
|
|
50
|
+
resolve_credential("${MYSQL_PASSWD}", section="mysql", field="passwd")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_plaintext_passthrough():
|
|
54
|
+
assert resolve_credential("plaintext-password", section="mysql", field="passwd") == "plaintext-password"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_none_passthrough():
|
|
58
|
+
assert resolve_credential(None, section="mysql", field="passwd") is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_partial_placeholder_not_substituted(monkeypatch):
|
|
62
|
+
monkeypatch.setenv("VAR", "value")
|
|
63
|
+
value = "prefix-${VAR}-suffix"
|
|
64
|
+
|
|
65
|
+
assert resolve_credential(value, section="mysql", field="passwd") == value
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Unit tests for QdrantVector role/api_key validation logic.
|
|
2
|
+
|
|
3
|
+
These raise before any network connection is attempted (see qdrant_vector.py:__init__),
|
|
4
|
+
so they need no live Qdrant server — not a fake-DB test, just constructor logic.
|
|
5
|
+
Design Ref: §4.2, §8.3 (client-side portion).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from echoss_db.qdrant_vector import QdrantVector
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_invalid_role_raises():
|
|
15
|
+
conn_info = {"qdrant": {"host": "localhost", "port": 6333}}
|
|
16
|
+
with pytest.raises(ValueError, match="role must be 'admin' or 'readonly'"):
|
|
17
|
+
QdrantVector(conn_info, role="superadmin")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_readonly_role_requires_read_only_api_key_field():
|
|
21
|
+
# api_key(admin)만 있고 read_only_api_key가 없는 상태에서 role="readonly" 요청 (FR-03).
|
|
22
|
+
conn_info = {"qdrant": {"host": "localhost", "port": 6333, "api_key": "admin-key"}}
|
|
23
|
+
with pytest.raises(ValueError, match="read_only_api_key"):
|
|
24
|
+
QdrantVector(conn_info, role="readonly")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_admin_role_requires_api_key_field_when_only_readonly_configured():
|
|
28
|
+
conn_info = {"qdrant": {"host": "localhost", "port": 6333, "read_only_api_key": "ro-key"}}
|
|
29
|
+
with pytest.raises(ValueError, match="qdrant.api_key"):
|
|
30
|
+
QdrantVector(conn_info, role="admin")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|