sql2api 0.1.0__py3-none-any.whl

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.
sql2api/store.py ADDED
@@ -0,0 +1,257 @@
1
+ """Persistence: the connection registry and the saved-query files (plain JSON on disk).
2
+
3
+ Everything that touches those files goes through here so that access stays confined to the
4
+ configured home folder and read-modify-write cycles are serialised.
5
+ """
6
+ import json
7
+ import os
8
+ import re
9
+ import threading
10
+ import uuid
11
+ from datetime import datetime
12
+
13
+ from . import config
14
+ from .errors import ApiError
15
+
16
+ # Serialises read-modify-write cycles on the JSON files this app manages.
17
+ lock = threading.RLock()
18
+
19
+ _ENV_REF_RE = re.compile(r'\$\{(\w+)\}')
20
+ _FILENAME_RE = re.compile(r'^[\w .\-]{1,100}$')
21
+
22
+
23
+ def write_json_atomic(path, data):
24
+ tmp_path = f'{path}.{uuid.uuid4().hex}.tmp'
25
+ with open(tmp_path, 'w') as f:
26
+ json.dump(data, f, indent=4)
27
+ os.replace(tmp_path, path)
28
+
29
+
30
+ def now():
31
+ return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
32
+
33
+
34
+ # --------------------------------------------------------------------------------------
35
+ # Connections
36
+ # --------------------------------------------------------------------------------------
37
+
38
+ def read_connections():
39
+ try:
40
+ with open(config.connections_file(), 'r') as f:
41
+ data = json.load(f)
42
+ except FileNotFoundError:
43
+ return {}
44
+ return data.get('connections', {})
45
+
46
+
47
+ def _expand_env(value):
48
+ """Replace ${VAR} references in a connection value with the environment variable's content."""
49
+ if not isinstance(value, str):
50
+ return value
51
+
52
+ def replace(match):
53
+ name = match.group(1)
54
+ if name not in os.environ:
55
+ raise ApiError(f"Environment variable '{name}' referenced by the connection is not set", 500)
56
+ return os.environ[name]
57
+
58
+ return _ENV_REF_RE.sub(replace, value)
59
+
60
+
61
+ def get_connection(connection_name):
62
+ """Return the usable (active, env-expanded) details of a named connection."""
63
+ if not isinstance(connection_name, str) or not connection_name:
64
+ raise ApiError('Connection name is missing')
65
+ details = read_connections().get(connection_name)
66
+ if not details:
67
+ raise ApiError(f"Connection '{connection_name}' not found", 404)
68
+ if not details.get('active', False):
69
+ raise ApiError(f"The connection '{connection_name}' is currently not active. "
70
+ "To use it, it must be set to active.", 403)
71
+ if details.get('db') not in config.SUPPORTED_DB_TYPES:
72
+ raise ApiError('Unsupported database type')
73
+ return {key: _expand_env(value) for key, value in details.items()}
74
+
75
+
76
+ def mask_passwords(connections):
77
+ """Hide stored passwords; ${VAR} references are not secrets and stay visible."""
78
+ def mask(conn):
79
+ password = conn.get('password')
80
+ if password and not _ENV_REF_RE.fullmatch(str(password)):
81
+ return {**conn, 'password': config.PASSWORD_MASK}
82
+ return conn
83
+
84
+ return {name: mask(conn) for name, conn in connections.items()}
85
+
86
+
87
+ def update_connections(connections):
88
+ for name, details in connections.items():
89
+ if not isinstance(details, dict) or details.get('db') not in config.SUPPORTED_DB_TYPES:
90
+ raise ApiError(f"Connection '{name}' must be an object whose 'db' is one of: "
91
+ f"{', '.join(config.SUPPORTED_DB_TYPES)}")
92
+ with lock:
93
+ existing = read_connections()
94
+ for name, details in connections.items():
95
+ # GET masks passwords, so a client echoing the mask back means "keep the current one".
96
+ if details.get('password') == config.PASSWORD_MASK and name in existing:
97
+ details = {**details, 'password': existing[name].get('password', '')}
98
+ existing[name] = details
99
+ config.home().mkdir(parents=True, exist_ok=True)
100
+ write_json_atomic(config.connections_file(), {'connections': existing})
101
+
102
+
103
+ def delete_connection(name):
104
+ with lock:
105
+ existing = read_connections()
106
+ if name not in existing:
107
+ raise ApiError(f"Connection '{name}' not found", 404)
108
+ del existing[name]
109
+ write_json_atomic(config.connections_file(), {'connections': existing})
110
+
111
+
112
+ # --------------------------------------------------------------------------------------
113
+ # Saved queries (all access is confined to the saved_sql folder)
114
+ # --------------------------------------------------------------------------------------
115
+
116
+ def saved_path_for_name(filename):
117
+ """Path of the saved-query file for a client-supplied name (without extension)."""
118
+ if not isinstance(filename, str) or not _FILENAME_RE.match(filename) or filename.startswith('.'):
119
+ raise ApiError("Filename may only contain letters, digits, spaces, '.', '_' and '-'")
120
+ return os.path.join(config.saved_sql_dir(), f'{filename}.json')
121
+
122
+
123
+ def resolve_saved_file(ref):
124
+ """Resolve a client-supplied file reference to an existing .json file inside the saved_sql folder.
125
+
126
+ Accepts a bare name ("cht" or "cht.json"), a path relative to the home folder
127
+ ("saved_sql/cht.json") or an absolute path - as long as it ends up in the saved_sql folder.
128
+ """
129
+ if not isinstance(ref, str) or not ref.strip():
130
+ raise ApiError('Filename is missing')
131
+ saved_dir = str(config.saved_sql_dir())
132
+ if os.path.isabs(ref):
133
+ candidate = ref
134
+ elif os.sep in ref or '/' in ref:
135
+ candidate = os.path.join(config.home(), ref)
136
+ else:
137
+ candidate = os.path.join(saved_dir, ref if ref.endswith('.json') else f'{ref}.json')
138
+ candidate = os.path.realpath(candidate)
139
+ if os.path.dirname(candidate) != os.path.realpath(saved_dir) or not candidate.endswith('.json'):
140
+ raise ApiError('Only .json files inside the saved_sql folder can be accessed', 403)
141
+ if not os.path.isfile(candidate):
142
+ raise ApiError('File not found', 404)
143
+ return candidate
144
+
145
+
146
+ def load_versions(path):
147
+ try:
148
+ with open(path, 'r') as f:
149
+ content = json.load(f)
150
+ except json.JSONDecodeError:
151
+ raise ApiError('Saved query file is not valid JSON', 500) from None
152
+ if not isinstance(content, dict):
153
+ raise ApiError('Saved query file has an unexpected structure', 500)
154
+ return content
155
+
156
+
157
+ def version_numbers(content):
158
+ return [int(v) for v in content if v.isdigit()]
159
+
160
+
161
+ def select_version(content, version=None):
162
+ """Return (number, data) for the requested version, or the latest one when none is given."""
163
+ if version is None:
164
+ numbers = version_numbers(content)
165
+ if not numbers:
166
+ raise ApiError('No valid versions found in the file')
167
+ version = max(numbers)
168
+ data = content.get(str(version))
169
+ if not isinstance(data, dict):
170
+ raise ApiError(f'Version {version} not found', 404)
171
+ return int(version), data
172
+
173
+
174
+ def save_version(filename, fields):
175
+ """Store ``fields`` as the next version of a saved query; returns (uuid, version number)."""
176
+ path = saved_path_for_name(filename)
177
+ query_uuid = str(uuid.uuid4())
178
+ timestamp = now()
179
+ with lock:
180
+ os.makedirs(config.saved_sql_dir(), exist_ok=True)
181
+ versions = load_versions(path) if os.path.exists(path) else {}
182
+ number = max(version_numbers(versions), default=0) + 1
183
+ versions[str(number)] = {
184
+ 'uuid': query_uuid,
185
+ **fields,
186
+ 'created_at': timestamp,
187
+ 'last_modified_at': timestamp,
188
+ 'status': 'active',
189
+ 'version': number,
190
+ 'execution_history': [],
191
+ }
192
+ write_json_atomic(path, versions)
193
+ return query_uuid, number
194
+
195
+
196
+ def delete_saved(ref, version=None):
197
+ """Delete a saved query file, or a single version of it (the file goes with its last version)."""
198
+ path = resolve_saved_file(ref)
199
+ with lock:
200
+ if version is None:
201
+ os.remove(path)
202
+ return
203
+ content = load_versions(path)
204
+ if str(version) not in content:
205
+ raise ApiError(f'Version {version} not found', 404)
206
+ del content[str(version)]
207
+ if version_numbers(content):
208
+ write_json_atomic(path, content)
209
+ else:
210
+ os.remove(path)
211
+
212
+
213
+ def record_execution(path, version, entry):
214
+ """Append to a saved version's execution_history (newest last, capped). Never raises."""
215
+ try:
216
+ with lock:
217
+ content = load_versions(path)
218
+ data = content.get(str(version))
219
+ if not isinstance(data, dict):
220
+ return
221
+ history = data.setdefault('execution_history', [])
222
+ history.append(entry)
223
+ del history[:-config.HISTORY_LIMIT]
224
+ write_json_atomic(path, content)
225
+ except (OSError, ApiError):
226
+ pass
227
+
228
+
229
+ def list_saved():
230
+ """Return every saved query with its versions' metadata (SQL text is not included)."""
231
+ saved_dir = str(config.saved_sql_dir())
232
+ if not os.path.isdir(saved_dir):
233
+ raise ApiError('Folder not found', 404)
234
+ files = []
235
+ for filename in os.listdir(saved_dir):
236
+ file_path = os.path.join(saved_dir, filename)
237
+ if not (os.path.isfile(file_path) and filename.endswith('.json')):
238
+ continue
239
+ try:
240
+ content = load_versions(file_path)
241
+ except ApiError:
242
+ continue
243
+ versions = [{
244
+ 'version': int(version),
245
+ 'author': data.get('author'),
246
+ 'description': data.get('description'),
247
+ 'tags': data.get('tags', []),
248
+ 'query_parameters': data.get('query_parameters', {}),
249
+ 'connection_name': data.get('connection_name'),
250
+ 'created_at': data.get('created_at'),
251
+ 'last_modified_at': data.get('last_modified_at'),
252
+ 'status': data.get('status'),
253
+ 'execution_history': data.get('execution_history', []),
254
+ } for version, data in content.items() if version.isdigit() and isinstance(data, dict)]
255
+ versions.sort(key=lambda v: v['version'])
256
+ files.append({'filename': filename[:-5], 'versions': versions})
257
+ return files
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: sql2api
3
+ Version: 0.1.0
4
+ Summary: Turn SQL into a REST API: run queries against MySQL, PostgreSQL, ClickHouse, SQLite or H2 over HTTP and get JSON, CSV, XML, YAML or XLSX back.
5
+ Author-email: Anantha Raju C <arcswdev@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AnanthaRajuC/SQL2API
8
+ Project-URL: Issues, https://github.com/AnanthaRajuC/SQL2API/issues
9
+ Project-URL: Changelog, https://github.com/AnanthaRajuC/SQL2API/blob/main/CHANGELOG.md
10
+ Keywords: sql,api,rest,flask,database,mysql,postgresql,clickhouse,sqlite,h2
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Flask
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: Flask>=2.2
22
+ Requires-Dist: PyYAML>=6.0
23
+ Requires-Dist: openpyxl>=3.1
24
+ Provides-Extra: mysql
25
+ Requires-Dist: mysql-connector-python>=8.0; extra == "mysql"
26
+ Provides-Extra: postgres
27
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
28
+ Provides-Extra: clickhouse
29
+ Requires-Dist: clickhouse-driver>=0.2; extra == "clickhouse"
30
+ Provides-Extra: h2
31
+ Requires-Dist: JayDeBeApi>=1.2; extra == "h2"
32
+ Requires-Dist: JPype1>=1.4; extra == "h2"
33
+ Provides-Extra: all
34
+ Requires-Dist: sql2api[clickhouse,h2,mysql,postgres]; extra == "all"
35
+ Provides-Extra: server
36
+ Requires-Dist: gunicorn>=21; extra == "server"
37
+ Provides-Extra: dev
38
+ Requires-Dist: sql2api[all]; extra == "dev"
39
+ Requires-Dist: ruff>=0.5; extra == "dev"
40
+ Dynamic: license-file
41
+
42
+ # SQL2API
43
+
44
+ [![CI](https://github.com/AnanthaRajuC/SQL2API/actions/workflows/ci.yml/badge.svg)](https://github.com/AnanthaRajuC/SQL2API/actions/workflows/ci.yml)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
46
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
47
+
48
+ **Turn SQL into a REST API.** SQL2API is a small Flask service that runs SQL against your databases and returns the
49
+ results as JSON, NDJSON, CSV, TSV, XML, YAML or Excel. Save a query once and it becomes an endpoint with typed,
50
+ injection-safe parameters, versioning and run history.
51
+
52
+ ~~~bash
53
+ $ curl 'http://127.0.0.1:5000/q/actor_by_id?id=7'
54
+ [{"actor_id": 7, "first_name": "GRACE", "last_name": "MOSTEL"}]
55
+
56
+ $ curl 'http://127.0.0.1:5000/q/films_by_rating?rating=PG&max_length=60&format=csv&page_size=2'
57
+ film_id,title,rating,length
58
+ 410,HEAVEN FREEDOM,PG,48
59
+ 443,HURRICANE AFFAIR,PG,49
60
+ ~~~
61
+
62
+ | Database | JSON | NDJSON | XML | YAML | CSV | TSV | XLSX |
63
+ |------------|:----:|:------:|:---:|:----:|:---:|:---:|:----:|
64
+ | MySQL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
65
+ | PostgreSQL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
66
+ | ClickHouse | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
67
+ | SQLite | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
68
+ | H2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
69
+
70
+ ## Features
71
+
72
+ - **Ad-hoc queries** - `POST /execute_sql` with SQL and a connection name.
73
+ - **Saved, versioned queries** - every save creates a new version; `GET /q/<name>?id=7` runs the latest one
74
+ (or `?version=1`). Each run is recorded in the query's execution history.
75
+ - **Bound parameters** - write `WHERE id = :id` and the value is sent to the database separately from the SQL, so it
76
+ cannot inject anything. Declare types (`{"id": "int"}`) and query-string values are converted for you.
77
+ - **Pagination** - `?page=2&page_size=50`, with `X-Has-More` telling you whether another page exists.
78
+ - **Read-only by default** - only single `SELECT`/`WITH`/`SHOW`/`DESCRIBE`/`EXPLAIN` statements run, and sessions are
79
+ opened read-only where the database supports it.
80
+ - **Secrets stay out of files** - `"password": "${PG_PASSWORD}"` in `db_connections.json` reads the environment.
81
+ - **Self-documenting** - OpenAPI at `/openapi.json`, Swagger UI at `/docs`.
82
+
83
+ ## Install
84
+
85
+ ~~~bash
86
+ pip install "sql2api[postgres]" # pick the drivers you need: mysql, postgres, clickhouse, h2
87
+ # or everything: pip install "sql2api[all]"
88
+ ~~~
89
+
90
+ SQLite needs no extra driver. H2 also needs a Java runtime (the H2 JDBC jar is bundled).
91
+ From a clone: `pip install -e ".[dev]"`. Or use Docker - see [below](#docker).
92
+
93
+ ## Quick start
94
+
95
+ The repository ships two sample SQLite databases and a couple of saved queries:
96
+
97
+ ~~~bash
98
+ cd examples
99
+ cp db_connections.example.json db_connections.json
100
+ sql2api serve # http://127.0.0.1:5000
101
+ ~~~
102
+
103
+ ~~~bash
104
+ curl -X POST 'http://127.0.0.1:5000/execute_sql?page_size=3' -H 'Content-Type: application/json' \
105
+ -d '{"sql": "SELECT * FROM actor WHERE actor_id > :min", "params": {"min": 10}, "connection_name": "sakila-sqlite"}'
106
+ ~~~
107
+
108
+ Open <http://127.0.0.1:5000/docs> for the interactive API reference.
109
+
110
+ For your own databases, run `sql2api init` in an empty folder: it creates `db_connections.json` (inactive templates for
111
+ every supported database) and `saved_sql/`. Edit the file, set `"active": true`, and start the server there.
112
+
113
+ ## Saving a query as an endpoint
114
+
115
+ ~~~bash
116
+ curl -X PATCH http://127.0.0.1:5000/save_sql_to_file -H 'Content-Type: application/json' -d '{
117
+ "filename": "actor_by_id",
118
+ "sql_query": "SELECT * FROM actor WHERE actor_id = :id",
119
+ "query_parameters": {"id": "int"},
120
+ "connection_name": "sakila-sqlite",
121
+ "author": "me", "description": "Look up an actor"
122
+ }'
123
+
124
+ curl 'http://127.0.0.1:5000/q/actor_by_id?id=7&format=yaml'
125
+ ~~~
126
+
127
+ Saving again under the same name adds version 2; `DELETE /saved_sql/actor_by_id?version=1` removes one version.
128
+
129
+ ## Configuration
130
+
131
+ Everything is configured through environment variables (all optional):
132
+
133
+ | Variable | Default | Effect |
134
+ |----------|---------|--------|
135
+ | `SQL2API_HOME` | current directory | Folder holding `db_connections.json` and `saved_sql/`. |
136
+ | `SQL2API_ALLOW_WRITES` | off | Allow `INSERT`/`UPDATE`/DDL. Otherwise only single read-only statements are accepted. |
137
+ | `SQL2API_API_KEY` | unset | When set, every request (except `/health` and `/docs`) needs a matching `X-API-Key` header. |
138
+ | `SQL2API_MAX_PAGE_SIZE` | `1000` | Upper limit for `page_size`. |
139
+ | `SQL2API_HOST` / `SQL2API_PORT` | `127.0.0.1` / `5000` | Bind address for `sql2api serve`. |
140
+ | `SQL2API_DEBUG` | off | Flask debug mode. Never enable on a reachable host. |
141
+ | `SQL2API_H2_JAR` | bundled | Path to a different H2 JDBC jar. |
142
+
143
+ ## Security
144
+
145
+ SQL2API runs whatever SQL it is given against your databases, so it ships locked down and expects you to finish the job:
146
+
147
+ - Set `SQL2API_API_KEY` and serve over TLS (put it behind a reverse proxy).
148
+ - Connect with a database account that only has the privileges the API needs - the read-only guard is
149
+ defence in depth, not a replacement for grants. (H2's driver cannot enforce read-only, so H2 relies on the guard.)
150
+ - Use bound `:name` parameters. The older `{name}` placeholders paste text into the SQL and are therefore restricted
151
+ to numbers and plain text.
152
+ - Saved-query files are only read from `saved_sql/`; passwords are never returned by the API.
153
+
154
+ See [SECURITY.md](SECURITY.md) to report a vulnerability.
155
+
156
+ ## Docker
157
+
158
+ ~~~bash
159
+ docker build -t sql2api . # add --build-arg WITH_H2=true for H2 support
160
+ docker run -p 5000:5000 -v "$PWD/data:/data" -e SQL2API_API_KEY=change-me sql2api
161
+ ~~~
162
+
163
+ The container keeps `db_connections.json` and `saved_sql/` in `/data`. It runs gunicorn with a single worker
164
+ (the files are protected by an in-process lock).
165
+
166
+ ## API overview
167
+
168
+ | Endpoint | Method | Purpose |
169
+ |----------|--------|---------|
170
+ | `/execute_sql` | POST | Run ad-hoc SQL (`sql`, `connection_name`, optional `params`). |
171
+ | `/q/<name>` | GET, POST | Run a saved query; query-string or body values become parameters. |
172
+ | `/save_sql_to_file` | PATCH | Save a query (creates the next version). |
173
+ | `/list_files` | GET | List saved queries and their versions (`sort_by`, `sort_order`). |
174
+ | `/saved_sql/<name>` | DELETE | Delete a saved query or one `?version=`. |
175
+ | `/view_file_content` | GET | Raw content of a saved query file. |
176
+ | `/execute_sql_from_file`, `/execute_sql_with_parameters_from_file` | POST | Run a saved query by `filepath` (same as `/q/<name>`). |
177
+ | `/connections` | GET, PATCH | List (passwords masked) / add / update connections. |
178
+ | `/connections/<name>` | DELETE | Remove a connection. |
179
+ | `/health`, `/docs`, `/openapi.json` | GET | Liveness, Swagger UI, OpenAPI spec. |
180
+
181
+ Full details are in [documentation/API.md](documentation/API.md).
182
+
183
+ ## Development
184
+
185
+ ~~~bash
186
+ pip install -e ".[dev]"
187
+ ruff check .
188
+ python -m unittest discover -s tests -t .
189
+ ~~~
190
+
191
+ The integration tests in `tests/test_integration.py` run against real MySQL, PostgreSQL, ClickHouse and H2 servers when
192
+ the matching `SQL2API_IT_*` variables are set, and are skipped otherwise; CI runs them against service containers.
193
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the pull request process, and [CHANGELOG.md](CHANGELOG.md) for what changed.
194
+
195
+ ## Third-party components
196
+
197
+ The wheel bundles the [H2 Database](https://h2database.com) JDBC driver (MPL 2.0 / EPL 1.0). The sample SQLite
198
+ databases in `examples/` derive from the Sakila and Chinook sample datasets.
199
+
200
+ ## License
201
+
202
+ [MIT](LICENSE) © Anantha Raju C
203
+
204
+ ## Contact
205
+
206
+ Anantha Raju C - [@anantharajuc](https://twitter.com/anantharajuc) - arcswdev@gmail.com
207
+
208
+ Project link: <https://github.com/AnanthaRajuC/SQL2API>
@@ -0,0 +1,19 @@
1
+ sql2api/__init__.py,sha256=FeKzfwtM7UszmZ7HBQ8AQi_WNHabfDeAIpWARwhqfys,185
2
+ sql2api/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ sql2api/app.py,sha256=VwoiN9TC2a8wPbCyf3IFMuWsnjteH5scrItLKIgbOoI,11447
4
+ sql2api/cli.py,sha256=1L3RYiCgnh1w_speXB4RPqxNWs5kR7kT68X9L-aqDLk,2712
5
+ sql2api/config.py,sha256=goaJF810BKzyeENR-6lc1l6k1OdSaF4-3CJkpMWv4gU,2222
6
+ sql2api/engine.py,sha256=b4uS0FZAWuUipr_CpAWjyVYPyzTtiDD2HDnfvI1PNP0,1551
7
+ sql2api/errors.py,sha256=nJddO1gAjxQZH7-IXf3Bt8ruxv3EhVw6t6viFKfluG4,264
8
+ sql2api/formats.py,sha256=SbLDZ-yQIt7pcWXYi8WNYEmG6JNqoeeGeRwIg5XHmos,4817
9
+ sql2api/openapi.py,sha256=evoO6KsPN0UjvdxYR0dwHTMygAI-7FIPxhURh7i4rXY,8597
10
+ sql2api/runners.py,sha256=tiP4-zNaD-bSdTycr1OBiHx00A-KESpLXXudhAhwBFw,5218
11
+ sql2api/sqltools.py,sha256=HPNC2hCSkBqiJ8O-WYWj7gwILnNOtyVD5A_58SG3lik,6657
12
+ sql2api/store.py,sha256=OHLE1PU7ZrqQcPJec4jJbqNeFBDSt7TOUO8NfH7QLJg,9890
13
+ sql2api/lib/h2-2.2.224.jar,sha256=udjxk1itqCpPbrWxdMbP4yCjdbWpy1pP5FbWI-blVJc,2614933
14
+ sql2api-0.1.0.dist-info/licenses/LICENSE,sha256=ufgSbLEx8zM6J0kFgDoK-6NNb0gqrsFfCb-CnNbZGg0,1071
15
+ sql2api-0.1.0.dist-info/METADATA,sha256=xrC-ppNP7rufNkYTdi_eFDmkieHx_jEjirZOFsf8F38,9461
16
+ sql2api-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
17
+ sql2api-0.1.0.dist-info/entry_points.txt,sha256=63gZjCA84qTkErE2iiuTiGS7GtdamvOqi2elEXwy_zE,45
18
+ sql2api-0.1.0.dist-info/top_level.txt,sha256=dicWZ6j0AE1xBDKr0-qh2CkuP7Yc1CWQf_T6r8LS6ms,8
19
+ sql2api-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sql2api = sql2api.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Anantha Raju C
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sql2api