neurocore-skill-postgres 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.
- neurocore_skill_postgres-0.1.0/.gitignore +152 -0
- neurocore_skill_postgres-0.1.0/PKG-INFO +50 -0
- neurocore_skill_postgres-0.1.0/README.md +27 -0
- neurocore_skill_postgres-0.1.0/pyproject.toml +39 -0
- neurocore_skill_postgres-0.1.0/src/neurocore_skill_postgres/__init__.py +5 -0
- neurocore_skill_postgres-0.1.0/src/neurocore_skill_postgres/skill.py +68 -0
- neurocore_skill_postgres-0.1.0/tests/test_skill.py +58 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
cover/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
db.sqlite3-journal
|
|
61
|
+
|
|
62
|
+
# Flask stuff:
|
|
63
|
+
instance/
|
|
64
|
+
.webassets-cache
|
|
65
|
+
|
|
66
|
+
# Scrapy stuff:
|
|
67
|
+
.scrapy
|
|
68
|
+
|
|
69
|
+
# Sphinx documentation
|
|
70
|
+
docs/_build/
|
|
71
|
+
|
|
72
|
+
# PyBuilder
|
|
73
|
+
.pybuilder/
|
|
74
|
+
target/
|
|
75
|
+
|
|
76
|
+
# Jupyter Notebook
|
|
77
|
+
.ipynb_checkpoints
|
|
78
|
+
|
|
79
|
+
# IPython
|
|
80
|
+
profile_default/
|
|
81
|
+
ipython_config.py
|
|
82
|
+
|
|
83
|
+
# pyenv
|
|
84
|
+
.python-version
|
|
85
|
+
|
|
86
|
+
# pipenv
|
|
87
|
+
Pipfile.lock
|
|
88
|
+
|
|
89
|
+
# poetry
|
|
90
|
+
poetry.lock
|
|
91
|
+
|
|
92
|
+
# pdm
|
|
93
|
+
.pdm.toml
|
|
94
|
+
.pdm-python
|
|
95
|
+
.pdm-build/
|
|
96
|
+
|
|
97
|
+
# PEP 582
|
|
98
|
+
__pypackages__/
|
|
99
|
+
|
|
100
|
+
# Celery stuff
|
|
101
|
+
celerybeat-schedule
|
|
102
|
+
celerybeat.pid
|
|
103
|
+
|
|
104
|
+
# SageMath parsed files
|
|
105
|
+
*.sage.py
|
|
106
|
+
|
|
107
|
+
# Environments
|
|
108
|
+
.env
|
|
109
|
+
.venv
|
|
110
|
+
env/
|
|
111
|
+
venv/
|
|
112
|
+
ENV/
|
|
113
|
+
env.bak/
|
|
114
|
+
venv.bak/
|
|
115
|
+
|
|
116
|
+
# Spyder project settings
|
|
117
|
+
.spyderproject
|
|
118
|
+
.spyproject
|
|
119
|
+
|
|
120
|
+
# Rope project settings
|
|
121
|
+
.ropeproject
|
|
122
|
+
|
|
123
|
+
# mkdocs documentation
|
|
124
|
+
/site
|
|
125
|
+
|
|
126
|
+
# mypy
|
|
127
|
+
.mypy_cache/
|
|
128
|
+
.dmypy.json
|
|
129
|
+
dmypy.json
|
|
130
|
+
|
|
131
|
+
# Pyre type checker
|
|
132
|
+
.pyre/
|
|
133
|
+
|
|
134
|
+
# pytype static type analyzer
|
|
135
|
+
.pytype/
|
|
136
|
+
|
|
137
|
+
# Cython debug symbols
|
|
138
|
+
cython_debug/
|
|
139
|
+
|
|
140
|
+
# Ruff
|
|
141
|
+
.ruff_cache/
|
|
142
|
+
|
|
143
|
+
# IDEs and editors
|
|
144
|
+
.idea/
|
|
145
|
+
.vscode/
|
|
146
|
+
*.swp
|
|
147
|
+
*.swo
|
|
148
|
+
*~
|
|
149
|
+
|
|
150
|
+
# OS files
|
|
151
|
+
.DS_Store
|
|
152
|
+
Thumbs.db
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neurocore-skill-postgres
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PostgreSQL query skill for NeuroCore
|
|
5
|
+
Author: NeuroCore Contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: ai,database,neurocore,postgres,skill,sql
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: neurocore-ai>=0.2.0
|
|
16
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# neurocore-skill-postgres
|
|
25
|
+
|
|
26
|
+
Run parameterized SQL against PostgreSQL from a NeuroCore flow (psycopg 3).
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install neurocore-skill-postgres
|
|
30
|
+
export DATABASE_URL=postgresql://user:pass@localhost/db
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
components:
|
|
35
|
+
- name: query
|
|
36
|
+
type: postgres
|
|
37
|
+
config:
|
|
38
|
+
sql: "SELECT id, name FROM users WHERE active = %s"
|
|
39
|
+
flow:
|
|
40
|
+
type: sequential
|
|
41
|
+
steps:
|
|
42
|
+
- component: query
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Provide `sql` (and optional `sql_params`) via config or context. Results are
|
|
46
|
+
written to `postgres_rows` (a list of row dicts; empty list for non-SELECT
|
|
47
|
+
statements, which are committed).
|
|
48
|
+
|
|
49
|
+
> Use parameterized queries (`%s` placeholders + `sql_params`) — never string
|
|
50
|
+
> interpolation — to avoid SQL injection.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# neurocore-skill-postgres
|
|
2
|
+
|
|
3
|
+
Run parameterized SQL against PostgreSQL from a NeuroCore flow (psycopg 3).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install neurocore-skill-postgres
|
|
7
|
+
export DATABASE_URL=postgresql://user:pass@localhost/db
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
components:
|
|
12
|
+
- name: query
|
|
13
|
+
type: postgres
|
|
14
|
+
config:
|
|
15
|
+
sql: "SELECT id, name FROM users WHERE active = %s"
|
|
16
|
+
flow:
|
|
17
|
+
type: sequential
|
|
18
|
+
steps:
|
|
19
|
+
- component: query
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Provide `sql` (and optional `sql_params`) via config or context. Results are
|
|
23
|
+
written to `postgres_rows` (a list of row dicts; empty list for non-SELECT
|
|
24
|
+
statements, which are committed).
|
|
25
|
+
|
|
26
|
+
> Use parameterized queries (`%s` placeholders + `sql_params`) — never string
|
|
27
|
+
> interpolation — to avoid SQL injection.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "neurocore-skill-postgres"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "PostgreSQL query skill for NeuroCore"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
authors = [{ name = "NeuroCore Contributors" }]
|
|
13
|
+
keywords = ["neurocore", "skill", "postgres", "sql", "database", "ai"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"neurocore-ai>=0.2.0",
|
|
24
|
+
"psycopg[binary]>=3.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=9.0.2", "pytest-asyncio>=0.24", "ruff>=0.8", "mypy>=1.8"]
|
|
29
|
+
|
|
30
|
+
[project.entry-points."neurocore.skills"]
|
|
31
|
+
postgres = "neurocore_skill_postgres:PostgresSkill"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/neurocore_skill_postgres"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
pythonpath = ["src"]
|
|
39
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""PostgresSkill — run a parameterized SQL query against PostgreSQL.
|
|
2
|
+
|
|
3
|
+
Reads ``sql`` (and optional ``sql_params``) from context (config provides
|
|
4
|
+
defaults) and writes returned rows to ``postgres_rows``.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from flowengine import FlowContext
|
|
13
|
+
|
|
14
|
+
from neurocore import AsyncSkill, SkillMeta
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PostgresSkill(AsyncSkill):
|
|
20
|
+
"""Async parameterized SQL execution against PostgreSQL (psycopg 3)."""
|
|
21
|
+
|
|
22
|
+
skill_meta = SkillMeta(
|
|
23
|
+
name="postgres",
|
|
24
|
+
version="0.1.0",
|
|
25
|
+
description="Run parameterized SQL against PostgreSQL",
|
|
26
|
+
author="NeuroCore Contributors",
|
|
27
|
+
requires=["psycopg[binary]>=3.1"],
|
|
28
|
+
provides=["postgres_rows"],
|
|
29
|
+
consumes=["sql", "sql_params"],
|
|
30
|
+
tags=["database", "sql", "postgres"],
|
|
31
|
+
config_schema={
|
|
32
|
+
"properties": {
|
|
33
|
+
"dsn": {"type": "string", "description": "Connection string."},
|
|
34
|
+
"sql": {"type": "string", "description": "Default SQL (overridable)."},
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def _resolve_dsn(self) -> str:
|
|
40
|
+
return self.config.get("dsn", "") or os.environ.get("DATABASE_URL", "")
|
|
41
|
+
|
|
42
|
+
async def _execute(self, sql: str, params: Any) -> list[dict[str, Any]]:
|
|
43
|
+
"""Execute ``sql`` and return rows as dicts (empty for non-SELECT)."""
|
|
44
|
+
import psycopg
|
|
45
|
+
from psycopg.rows import dict_row
|
|
46
|
+
|
|
47
|
+
async with await psycopg.AsyncConnection.connect(
|
|
48
|
+
self._resolve_dsn(), row_factory=dict_row
|
|
49
|
+
) as conn, conn.cursor() as cur:
|
|
50
|
+
await cur.execute(sql, params)
|
|
51
|
+
if cur.description is None:
|
|
52
|
+
await conn.commit()
|
|
53
|
+
return []
|
|
54
|
+
return [dict(row) for row in await cur.fetchall()]
|
|
55
|
+
|
|
56
|
+
async def process(self, context: FlowContext) -> FlowContext:
|
|
57
|
+
sql = str(context.get("sql") or self.config.get("sql", ""))
|
|
58
|
+
if not sql:
|
|
59
|
+
logger.warning("PostgresSkill: no 'sql' provided.")
|
|
60
|
+
context.set("postgres_rows", [])
|
|
61
|
+
return context
|
|
62
|
+
params = context.get("sql_params")
|
|
63
|
+
try:
|
|
64
|
+
context.set("postgres_rows", await self._execute(sql, params))
|
|
65
|
+
except Exception as exc: # noqa: BLE001
|
|
66
|
+
logger.error("PostgresSkill query failed: %s", exc, exc_info=True)
|
|
67
|
+
context.set("postgres_rows", {"error": str(exc)})
|
|
68
|
+
return context
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tests for PostgresSkill (DB call mocked via the _execute seam)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from flowengine import FlowContext
|
|
5
|
+
from neurocore_skill_postgres import PostgresSkill
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_skill_meta():
|
|
9
|
+
assert PostgresSkill.skill_meta.name == "postgres"
|
|
10
|
+
assert "postgres_rows" in PostgresSkill.skill_meta.provides
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def test_process_returns_rows(monkeypatch):
|
|
14
|
+
skill = PostgresSkill()
|
|
15
|
+
skill.init({"dsn": "postgresql://x"})
|
|
16
|
+
|
|
17
|
+
async def fake_execute(sql, params):
|
|
18
|
+
assert "select" in sql.lower()
|
|
19
|
+
return [{"id": 1, "name": "a"}]
|
|
20
|
+
|
|
21
|
+
monkeypatch.setattr(skill, "_execute", fake_execute)
|
|
22
|
+
ctx = FlowContext()
|
|
23
|
+
ctx.set("sql", "SELECT * FROM t")
|
|
24
|
+
out = await skill.process(ctx)
|
|
25
|
+
assert out.get("postgres_rows")[0]["name"] == "a"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def test_config_sql_default(monkeypatch):
|
|
29
|
+
skill = PostgresSkill()
|
|
30
|
+
skill.init({"dsn": "postgresql://x", "sql": "SELECT 1"})
|
|
31
|
+
|
|
32
|
+
async def fake_execute(sql, params):
|
|
33
|
+
return [{"?column?": 1}]
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(skill, "_execute", fake_execute)
|
|
36
|
+
out = await skill.process(FlowContext())
|
|
37
|
+
assert out.get("postgres_rows") == [{"?column?": 1}]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def test_no_sql_returns_empty():
|
|
41
|
+
skill = PostgresSkill()
|
|
42
|
+
skill.init({"dsn": "postgresql://x"})
|
|
43
|
+
out = await skill.process(FlowContext())
|
|
44
|
+
assert out.get("postgres_rows") == []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def test_error_sets_sentinel(monkeypatch):
|
|
48
|
+
skill = PostgresSkill()
|
|
49
|
+
skill.init({"dsn": "postgresql://x"})
|
|
50
|
+
|
|
51
|
+
async def boom(sql, params):
|
|
52
|
+
raise RuntimeError("connection refused")
|
|
53
|
+
|
|
54
|
+
monkeypatch.setattr(skill, "_execute", boom)
|
|
55
|
+
ctx = FlowContext()
|
|
56
|
+
ctx.set("sql", "SELECT 1")
|
|
57
|
+
out = await skill.process(ctx)
|
|
58
|
+
assert "error" in out.get("postgres_rows")
|