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.
@@ -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,5 @@
1
+ """neurocore-skill-postgres — PostgreSQL query skill for NeuroCore."""
2
+
3
+ from neurocore_skill_postgres.skill import PostgresSkill
4
+
5
+ __all__ = ["PostgresSkill"]
@@ -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")