qmcp 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.
- qmcp-0.1.0/LICENSE +21 -0
- qmcp-0.1.0/PKG-INFO +84 -0
- qmcp-0.1.0/README.md +66 -0
- qmcp-0.1.0/pyproject.toml +26 -0
- qmcp-0.1.0/qmcp/__init__.py +3 -0
- qmcp-0.1.0/qmcp/qlib.py +143 -0
- qmcp-0.1.0/qmcp/qpython/__init__.py +69 -0
- qmcp-0.1.0/qmcp/qpython/_pandas.py +221 -0
- qmcp-0.1.0/qmcp/qpython/qcollection.py +474 -0
- qmcp-0.1.0/qmcp/qpython/qconnection.py +386 -0
- qmcp-0.1.0/qmcp/qpython/qreader.py +566 -0
- qmcp-0.1.0/qmcp/qpython/qtemporal.py +436 -0
- qmcp-0.1.0/qmcp/qpython/qtype.py +418 -0
- qmcp-0.1.0/qmcp/qpython/qwriter.py +284 -0
- qmcp-0.1.0/qmcp/qpython/utils.py +73 -0
- qmcp-0.1.0/qmcp/qpython_compat.py +51 -0
- qmcp-0.1.0/qmcp/server.py +90 -0
- qmcp-0.1.0/qmcp/sugar/__init__.py +1 -0
- qmcp-0.1.0/qmcp/sugar/sugar.py +386 -0
- qmcp-0.1.0/qmcp.egg-info/PKG-INFO +84 -0
- qmcp-0.1.0/qmcp.egg-info/SOURCES.txt +24 -0
- qmcp-0.1.0/qmcp.egg-info/dependency_links.txt +1 -0
- qmcp-0.1.0/qmcp.egg-info/entry_points.txt +2 -0
- qmcp-0.1.0/qmcp.egg-info/requires.txt +4 -0
- qmcp-0.1.0/qmcp.egg-info/top_level.txt +1 -0
- qmcp-0.1.0/setup.cfg +4 -0
qmcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gabi Teodoru
|
|
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.
|
qmcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qmcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP Server for q/kdb+ integration
|
|
5
|
+
Author-email: Gabi Teodoru <gabiteodoru@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gabiteodoru/qmcp
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/gabiteodoru/qmcp/issues
|
|
9
|
+
Project-URL: Source Code, https://github.com/gabiteodoru/qmcp
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: mcp
|
|
14
|
+
Requires-Dist: numpy>=1.8.0
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: more_itertools
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# qmcp Server
|
|
20
|
+
|
|
21
|
+
A Model Context Protocol server for q/kdb+ integration.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Connect to q/kdb+ servers
|
|
26
|
+
- Execute q queries and commands
|
|
27
|
+
- Persistent connection management
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.8+
|
|
32
|
+
- qpython3 package
|
|
33
|
+
- Access to a q/kdb+ server
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Run the MCP server:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
qmcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Environment Variables
|
|
50
|
+
|
|
51
|
+
- `Q_DEFAULT_HOST` - Default connection info in format: `host`, `host:port`, or `host:port:user:passwd`
|
|
52
|
+
|
|
53
|
+
### Connection Fallback Logic
|
|
54
|
+
|
|
55
|
+
The `connect_to_q(host)` tool uses flexible fallback logic:
|
|
56
|
+
|
|
57
|
+
1. **Full connection string** (has colons): Use directly, ignore `Q_DEFAULT_HOST`
|
|
58
|
+
- `connect_to_q("myhost:5001:user:pass")`
|
|
59
|
+
2. **Port number only**: Combine with `Q_DEFAULT_HOST` or use `localhost`
|
|
60
|
+
- `connect_to_q(5001)` → Uses `Q_DEFAULT_HOST` settings with port 5001
|
|
61
|
+
3. **No parameters**: Use `Q_DEFAULT_HOST` directly
|
|
62
|
+
- `connect_to_q()` → Uses `Q_DEFAULT_HOST` as-is
|
|
63
|
+
4. **Hostname only**: Use as hostname with `Q_DEFAULT_HOST` port/auth or default port
|
|
64
|
+
- `connect_to_q("myhost")` → Combines with `Q_DEFAULT_HOST` settings
|
|
65
|
+
|
|
66
|
+
### Tools
|
|
67
|
+
|
|
68
|
+
1. `connect_to_q(host=None)` - Connect to q server with fallback logic
|
|
69
|
+
2. `query_q(command)` - Execute q commands and return results
|
|
70
|
+
|
|
71
|
+
### Known Limitations
|
|
72
|
+
|
|
73
|
+
When using the MCP server, be aware of these limitations:
|
|
74
|
+
|
|
75
|
+
- **Keyed tables**: Operations like `1!table` may fail during pandas conversion
|
|
76
|
+
- **String vs Symbol distinction**: q strings and symbols may appear identical in output
|
|
77
|
+
- **Type ambiguity**: Use q's `meta` and `type` commands to determine actual data types when precision matters
|
|
78
|
+
- **Pandas conversion**: Some q-specific data structures may not convert properly to pandas DataFrames
|
|
79
|
+
|
|
80
|
+
For type checking, use:
|
|
81
|
+
```q
|
|
82
|
+
meta table / Check table column types and structure
|
|
83
|
+
type variable / Check variable type
|
|
84
|
+
```
|
qmcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# qmcp Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol server for q/kdb+ integration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Connect to q/kdb+ servers
|
|
8
|
+
- Execute q queries and commands
|
|
9
|
+
- Persistent connection management
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python 3.8+
|
|
14
|
+
- qpython3 package
|
|
15
|
+
- Access to a q/kdb+ server
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Run the MCP server:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
qmcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Environment Variables
|
|
32
|
+
|
|
33
|
+
- `Q_DEFAULT_HOST` - Default connection info in format: `host`, `host:port`, or `host:port:user:passwd`
|
|
34
|
+
|
|
35
|
+
### Connection Fallback Logic
|
|
36
|
+
|
|
37
|
+
The `connect_to_q(host)` tool uses flexible fallback logic:
|
|
38
|
+
|
|
39
|
+
1. **Full connection string** (has colons): Use directly, ignore `Q_DEFAULT_HOST`
|
|
40
|
+
- `connect_to_q("myhost:5001:user:pass")`
|
|
41
|
+
2. **Port number only**: Combine with `Q_DEFAULT_HOST` or use `localhost`
|
|
42
|
+
- `connect_to_q(5001)` → Uses `Q_DEFAULT_HOST` settings with port 5001
|
|
43
|
+
3. **No parameters**: Use `Q_DEFAULT_HOST` directly
|
|
44
|
+
- `connect_to_q()` → Uses `Q_DEFAULT_HOST` as-is
|
|
45
|
+
4. **Hostname only**: Use as hostname with `Q_DEFAULT_HOST` port/auth or default port
|
|
46
|
+
- `connect_to_q("myhost")` → Combines with `Q_DEFAULT_HOST` settings
|
|
47
|
+
|
|
48
|
+
### Tools
|
|
49
|
+
|
|
50
|
+
1. `connect_to_q(host=None)` - Connect to q server with fallback logic
|
|
51
|
+
2. `query_q(command)` - Execute q commands and return results
|
|
52
|
+
|
|
53
|
+
### Known Limitations
|
|
54
|
+
|
|
55
|
+
When using the MCP server, be aware of these limitations:
|
|
56
|
+
|
|
57
|
+
- **Keyed tables**: Operations like `1!table` may fail during pandas conversion
|
|
58
|
+
- **String vs Symbol distinction**: q strings and symbols may appear identical in output
|
|
59
|
+
- **Type ambiguity**: Use q's `meta` and `type` commands to determine actual data types when precision matters
|
|
60
|
+
- **Pandas conversion**: Some q-specific data structures may not convert properly to pandas DataFrames
|
|
61
|
+
|
|
62
|
+
For type checking, use:
|
|
63
|
+
```q
|
|
64
|
+
meta table / Check table column types and structure
|
|
65
|
+
type variable / Check variable type
|
|
66
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qmcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP Server for q/kdb+ integration"
|
|
9
|
+
authors = [{name = "Gabi Teodoru", email = "gabiteodoru@gmail.com"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"mcp",
|
|
15
|
+
"numpy>=1.8.0",
|
|
16
|
+
"pandas",
|
|
17
|
+
"more_itertools",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
"Homepage" = "https://github.com/gabiteodoru/qmcp"
|
|
22
|
+
"Bug Tracker" = "https://github.com/gabiteodoru/qmcp/issues"
|
|
23
|
+
"Source Code" = "https://github.com/gabiteodoru/qmcp"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
qmcp = "qmcp.server:main"
|
qmcp-0.1.0/qmcp/qlib.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Q/kdb+ connection utilities for qmcp
|
|
3
|
+
Clean, minimal interface for connecting to and querying q servers
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .qpython_compat import QConnection, MetaData
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import socket
|
|
10
|
+
from .sugar import dmap, spl
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_hostname():
|
|
16
|
+
"""Get current hostname"""
|
|
17
|
+
return socket.gethostname()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _QDecodeWrapper:
|
|
21
|
+
"""Wrapper for QConnection that handles pandas DataFrame encoding/decoding"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, q):
|
|
24
|
+
self.q = q
|
|
25
|
+
|
|
26
|
+
def __call__(self, *args, **kwargs):
|
|
27
|
+
# Handle DataFrame arguments
|
|
28
|
+
for arg in args:
|
|
29
|
+
if isinstance(arg, pd.DataFrame):
|
|
30
|
+
if not hasattr(arg, 'meta'):
|
|
31
|
+
arg.meta = MetaData(qtype=98)
|
|
32
|
+
|
|
33
|
+
# Set date column metadata
|
|
34
|
+
for c in 'd, date'-spl:
|
|
35
|
+
if (c in arg.columns and
|
|
36
|
+
isinstance(arg[c].dtype, np.dtype) and
|
|
37
|
+
arg[c].dtype != np.dtype(np.object_) and
|
|
38
|
+
c not in arg.meta.__dict__):
|
|
39
|
+
arg.meta[c] = 14
|
|
40
|
+
|
|
41
|
+
# Handle string columns
|
|
42
|
+
if 'strcols' in kwargs:
|
|
43
|
+
for sc in kwargs['strcols']-spl:
|
|
44
|
+
arg.meta[sc] = 0
|
|
45
|
+
|
|
46
|
+
# Execute query
|
|
47
|
+
r = self.q(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
# Decode string/symbol columns in result
|
|
50
|
+
if type(r) == pd.DataFrame:
|
|
51
|
+
rd = r.meta.as_dict()
|
|
52
|
+
for col in rd:
|
|
53
|
+
if rd[col] in (0, 11): # string or symbol columns
|
|
54
|
+
r[col] = r[col].map(dmap(lambda x: x.decode('utf-8'), r[col].drop_duplicates()))
|
|
55
|
+
|
|
56
|
+
return r
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def connect_to_q(host=None):
|
|
60
|
+
"""
|
|
61
|
+
Connect to q server with flexible fallback logic for MCP
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
host: None, port number, 'host:port', or full connection string
|
|
65
|
+
|
|
66
|
+
Fallback logic:
|
|
67
|
+
1. If host has colons, use directly (ignores Q_DEFAULT_HOST)
|
|
68
|
+
2. If no envvar and no parameter, fail
|
|
69
|
+
3. If port provided, combine with Q_DEFAULT_HOST or localhost
|
|
70
|
+
4. Use Q_DEFAULT_HOST if available
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
_QDecodeWrapper instance
|
|
74
|
+
"""
|
|
75
|
+
# 4) If host has colons, use it directly (ignore Q_DEFAULT_HOST)
|
|
76
|
+
if host and ':' in str(host):
|
|
77
|
+
return _qConnect(str(host), True)
|
|
78
|
+
|
|
79
|
+
# Get Q_DEFAULT_HOST environment variable
|
|
80
|
+
default_host = os.environ.get('Q_DEFAULT_HOST')
|
|
81
|
+
|
|
82
|
+
# 2) Fail if no envvar and no parameter
|
|
83
|
+
if not default_host and not host:
|
|
84
|
+
raise ValueError("No connection info: set Q_DEFAULT_HOST or provide host parameter")
|
|
85
|
+
|
|
86
|
+
# 3) If host is just a port number, combine with default host info
|
|
87
|
+
if host and str(host).isdigit():
|
|
88
|
+
port = str(host)
|
|
89
|
+
if default_host:
|
|
90
|
+
# Extract host[:user:passwd] from Q_DEFAULT_HOST, replace port
|
|
91
|
+
parts = default_host.split(':')
|
|
92
|
+
if len(parts) >= 2:
|
|
93
|
+
# Replace port in Q_DEFAULT_HOST
|
|
94
|
+
parts[1] = port
|
|
95
|
+
return _qConnect(':'.join(parts), True)
|
|
96
|
+
else:
|
|
97
|
+
# Q_DEFAULT_HOST is just hostname
|
|
98
|
+
return _qConnect(f"{default_host}:{port}", True)
|
|
99
|
+
else:
|
|
100
|
+
# No Q_DEFAULT_HOST, use localhost
|
|
101
|
+
return _qConnect(f"localhost:{port}", True)
|
|
102
|
+
|
|
103
|
+
# 1) Use Q_DEFAULT_HOST (host, host:port, or host:port:user:passwd)
|
|
104
|
+
if default_host:
|
|
105
|
+
return _qConnect(default_host, True)
|
|
106
|
+
|
|
107
|
+
# Should never reach here due to check above
|
|
108
|
+
raise ValueError("Invalid connection parameters")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _qConnect(qCredentials, pandas):
|
|
112
|
+
"""
|
|
113
|
+
Connect to q server with retry logic
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
qCredentials: 'host:port' or 'host:port:user:passwd'
|
|
117
|
+
pandas: return pandas-enabled connection
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
QConnection or _QDecodeWrapper
|
|
121
|
+
"""
|
|
122
|
+
qCreds = tuple(qCredentials.split(':'))
|
|
123
|
+
host, port, user, passwd = qCreds if len(qCreds) == 4 else (qCreds + (None, None))
|
|
124
|
+
port = int(port)
|
|
125
|
+
|
|
126
|
+
if host == _get_hostname():
|
|
127
|
+
host = 'localhost'
|
|
128
|
+
|
|
129
|
+
# Retry connection for up to 5 seconds
|
|
130
|
+
t = time.time()
|
|
131
|
+
while time.time() - t < 5:
|
|
132
|
+
try:
|
|
133
|
+
q = QConnection(host, port, user, passwd, pandas=pandas)
|
|
134
|
+
q.open()
|
|
135
|
+
return _QDecodeWrapper(q) if pandas else q
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"Connection attempt failed: {e}")
|
|
138
|
+
time.sleep(0.1)
|
|
139
|
+
|
|
140
|
+
# Final attempt without retry
|
|
141
|
+
q = QConnection(host, port, user, passwd, pandas=pandas)
|
|
142
|
+
q.open()
|
|
143
|
+
return _QDecodeWrapper(q) if pandas else q
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2011-2014 Exxeleron GmbH
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
__all__ = ['qconnection', 'qtype', 'qtemporal', 'qcollection']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__version__ = '2.0.0'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from qpython.fastutils import uncompress
|
|
26
|
+
except:
|
|
27
|
+
__is_cython_enabled__ = False
|
|
28
|
+
else:
|
|
29
|
+
__is_cython_enabled__ = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MetaData(object):
|
|
33
|
+
'''Utility class for enriching data structures with meta data, e.g. qtype hint.'''
|
|
34
|
+
def __init__(self, **kw):
|
|
35
|
+
self.__dict__.update(kw)
|
|
36
|
+
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
if not self.__dict__.items():
|
|
39
|
+
return 'metadata()'
|
|
40
|
+
|
|
41
|
+
s = ['metadata(']
|
|
42
|
+
for k, v in self.__dict__.items():
|
|
43
|
+
s.append('%s=%s' % (k, repr(v)))
|
|
44
|
+
s.append(', ')
|
|
45
|
+
s[-1] = ')'
|
|
46
|
+
return ''.join(s)
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, attr):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def __getitem__(self, key):
|
|
52
|
+
return self.__dict__.get(key, None)
|
|
53
|
+
|
|
54
|
+
def __setitem__(self, key, value):
|
|
55
|
+
self.__dict__[key] = value
|
|
56
|
+
|
|
57
|
+
def as_dict(self):
|
|
58
|
+
return self.__dict__.copy()
|
|
59
|
+
|
|
60
|
+
def union_dict(self, **kw):
|
|
61
|
+
return dict(list(self.as_dict().items()) + list(kw.items()))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
CONVERSION_OPTIONS = MetaData(raw = False,
|
|
66
|
+
numpy_temporals = False,
|
|
67
|
+
pandas = False,
|
|
68
|
+
single_char_strings = False
|
|
69
|
+
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2011-2014 Exxeleron GmbH
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
import pandas
|
|
18
|
+
import struct
|
|
19
|
+
import sys
|
|
20
|
+
if sys.version > '3':
|
|
21
|
+
basestring = (str, bytes)
|
|
22
|
+
|
|
23
|
+
from collections import OrderedDict
|
|
24
|
+
|
|
25
|
+
from qpython import MetaData
|
|
26
|
+
from qpython.qreader import QReader, QReaderException
|
|
27
|
+
from qpython.qcollection import QDictionary, qlist
|
|
28
|
+
from qpython.qwriter import QWriter, QWriterException
|
|
29
|
+
from qpython.qtype import *
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PandasQReader(QReader):
|
|
34
|
+
|
|
35
|
+
_reader_map = dict.copy(QReader._reader_map)
|
|
36
|
+
parse = Mapper(_reader_map)
|
|
37
|
+
|
|
38
|
+
@parse(QDICTIONARY)
|
|
39
|
+
def _read_dictionary(self, qtype = QDICTIONARY):
|
|
40
|
+
if self._options.pandas:
|
|
41
|
+
keys = self._read_object()
|
|
42
|
+
values = self._read_object()
|
|
43
|
+
|
|
44
|
+
if isinstance(keys, pandas.DataFrame):
|
|
45
|
+
if not isinstance(values, pandas.DataFrame):
|
|
46
|
+
raise QReaderException('Keyed table creation: values are expected to be of type pandas.DataFrame. Actual: %s' % type(values))
|
|
47
|
+
|
|
48
|
+
indices = keys.columns
|
|
49
|
+
table = keys
|
|
50
|
+
table.meta = keys.meta
|
|
51
|
+
table.meta.qtype = QKEYED_TABLE
|
|
52
|
+
|
|
53
|
+
for column in values.columns:
|
|
54
|
+
table[column] = values[column]
|
|
55
|
+
table.meta[column] = values.meta[column]
|
|
56
|
+
|
|
57
|
+
table.set_index([column for column in indices], inplace = True)
|
|
58
|
+
|
|
59
|
+
return table
|
|
60
|
+
else:
|
|
61
|
+
keys = keys if not isinstance(keys, pandas.Series) else keys.values
|
|
62
|
+
values = values if not isinstance(values, pandas.Series) else values.values
|
|
63
|
+
return QDictionary(keys, values)
|
|
64
|
+
else:
|
|
65
|
+
return QReader._read_dictionary(self, qtype = qtype)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@parse(QTABLE)
|
|
69
|
+
def _read_table(self, qtype = QTABLE):
|
|
70
|
+
if self._options.pandas:
|
|
71
|
+
self._buffer.skip() # ignore attributes
|
|
72
|
+
self._buffer.skip() # ignore dict type stamp
|
|
73
|
+
|
|
74
|
+
columns = self._read_object()
|
|
75
|
+
self._buffer.skip() # ignore generic list type indicator
|
|
76
|
+
data = QReader._read_general_list(self, qtype)
|
|
77
|
+
|
|
78
|
+
odict = OrderedDict()
|
|
79
|
+
meta = MetaData(qtype = QTABLE)
|
|
80
|
+
for i in range(len(columns)):
|
|
81
|
+
column_name = columns[i] if isinstance(columns[i], str) else columns[i].decode("utf-8")
|
|
82
|
+
if isinstance(data[i], str):
|
|
83
|
+
# convert character list (represented as string) to numpy representation
|
|
84
|
+
meta[column_name] = QSTRING
|
|
85
|
+
odict[column_name] = pandas.Series(list(data[i]), dtype = numpy.str).replace(b' ', numpy.nan)
|
|
86
|
+
elif isinstance(data[i], bytes):
|
|
87
|
+
# convert character list (represented as string) to numpy representation
|
|
88
|
+
meta[column_name] = QSTRING
|
|
89
|
+
odict[column_name] = pandas.Series(list(data[i].decode()), dtype = str).replace(b' ', numpy.nan)
|
|
90
|
+
elif isinstance(data[i], (list, tuple)):
|
|
91
|
+
meta[column_name] = QGENERAL_LIST
|
|
92
|
+
tarray = numpy.ndarray(shape = len(data[i]), dtype = numpy.dtype('O'))
|
|
93
|
+
for j in range(len(data[i])):
|
|
94
|
+
tarray[j] = data[i][j]
|
|
95
|
+
odict[column_name] = tarray
|
|
96
|
+
else:
|
|
97
|
+
meta[column_name] = data[i].meta.qtype
|
|
98
|
+
odict[column_name] = data[i]
|
|
99
|
+
|
|
100
|
+
df = pandas.DataFrame(odict)
|
|
101
|
+
df._metadata = ["meta"]
|
|
102
|
+
df.meta = meta
|
|
103
|
+
return df
|
|
104
|
+
else:
|
|
105
|
+
return QReader._read_table(self, qtype = qtype)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _read_list(self, qtype):
|
|
109
|
+
if self._options.pandas:
|
|
110
|
+
self._options.numpy_temporals = True
|
|
111
|
+
|
|
112
|
+
qlist = QReader._read_list(self, qtype = qtype)
|
|
113
|
+
|
|
114
|
+
if self._options.pandas:
|
|
115
|
+
if -abs(qtype) not in [QBOOL, QMONTH, QDATE, QDATETIME, QMINUTE, QSECOND, QTIME, QTIMESTAMP, QTIMESPAN, QSYMBOL]:
|
|
116
|
+
null = QNULLMAP[-abs(qtype)][1]
|
|
117
|
+
ps = pandas.Series(data = qlist).replace(null, numpy.nan)
|
|
118
|
+
else:
|
|
119
|
+
ps = pandas.Series(data = qlist)
|
|
120
|
+
|
|
121
|
+
ps.meta = MetaData(qtype = qtype)
|
|
122
|
+
return ps
|
|
123
|
+
else:
|
|
124
|
+
return qlist
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@parse(QGENERAL_LIST)
|
|
128
|
+
def _read_general_list(self, qtype = QGENERAL_LIST):
|
|
129
|
+
qlist = QReader._read_general_list(self, qtype)
|
|
130
|
+
if self._options.pandas:
|
|
131
|
+
return [numpy.nan if isinstance(element, basestring) and element == b' ' else element for element in qlist]
|
|
132
|
+
else:
|
|
133
|
+
return qlist
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PandasQWriter(QWriter):
|
|
138
|
+
|
|
139
|
+
_writer_map = dict.copy(QWriter._writer_map)
|
|
140
|
+
serialize = Mapper(_writer_map)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@serialize(pandas.Series)
|
|
144
|
+
def _write_pandas_series(self, data, qtype = None):
|
|
145
|
+
if qtype is not None:
|
|
146
|
+
qtype = -abs(qtype)
|
|
147
|
+
|
|
148
|
+
if qtype is None and hasattr(data, 'meta'):
|
|
149
|
+
qtype = -abs(data.meta.qtype)
|
|
150
|
+
|
|
151
|
+
if data.dtype == '|S1':
|
|
152
|
+
qtype = QCHAR
|
|
153
|
+
|
|
154
|
+
if qtype is None:
|
|
155
|
+
qtype = Q_TYPE.get(data.dtype.type, None)
|
|
156
|
+
|
|
157
|
+
if qtype is None and data.dtype.type in (numpy.datetime64, numpy.timedelta64):
|
|
158
|
+
qtype = TEMPORAL_PY_TYPE.get(str(data.dtype), None)
|
|
159
|
+
|
|
160
|
+
if qtype is None:
|
|
161
|
+
# determinate type based on first element of the numpy array
|
|
162
|
+
qtype = Q_TYPE.get(type(data.iloc[0]), QGENERAL_LIST)
|
|
163
|
+
|
|
164
|
+
if qtype == QSTRING:
|
|
165
|
+
# assume we have a generic list of strings -> force representation as symbol list
|
|
166
|
+
qtype = QSYMBOL
|
|
167
|
+
|
|
168
|
+
if qtype is None:
|
|
169
|
+
raise QWriterException('Unable to serialize pandas series %s' % data)
|
|
170
|
+
|
|
171
|
+
if qtype == QGENERAL_LIST:
|
|
172
|
+
self._write_generic_list(data.values)
|
|
173
|
+
elif qtype == QCHAR:
|
|
174
|
+
self._write_string(data.replace(numpy.nan, ' ').values.astype(numpy.bytes_).tobytes())
|
|
175
|
+
elif data.dtype.type not in (numpy.datetime64, numpy.timedelta64):
|
|
176
|
+
data = data.fillna(QNULLMAP[-abs(qtype)][1])
|
|
177
|
+
data = data.values
|
|
178
|
+
|
|
179
|
+
if PY_TYPE[qtype] != data.dtype:
|
|
180
|
+
data = data.astype(PY_TYPE[qtype])
|
|
181
|
+
|
|
182
|
+
self._write_list(data, qtype = qtype)
|
|
183
|
+
else:
|
|
184
|
+
data = data.values
|
|
185
|
+
data = data.astype(TEMPORAL_Q_TYPE[qtype])
|
|
186
|
+
self._write_list(data, qtype = qtype)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@serialize(pandas.DataFrame)
|
|
190
|
+
def _write_pandas_data_frame(self, data, qtype = None):
|
|
191
|
+
data_columns = data.columns.values
|
|
192
|
+
|
|
193
|
+
if hasattr(data, 'meta') and data.meta.qtype == QKEYED_TABLE:
|
|
194
|
+
# data frame represents keyed table
|
|
195
|
+
self._buffer.write(struct.pack('=b', QDICTIONARY))
|
|
196
|
+
self._buffer.write(struct.pack('=bxb', QTABLE, QDICTIONARY))
|
|
197
|
+
index_columns = data.index.names
|
|
198
|
+
self._write(qlist(numpy.array(index_columns), qtype = QSYMBOL_LIST))
|
|
199
|
+
data.reset_index(inplace = True)
|
|
200
|
+
self._buffer.write(struct.pack('=bxi', QGENERAL_LIST, len(index_columns)))
|
|
201
|
+
for column in index_columns:
|
|
202
|
+
self._write_pandas_series(data[column], qtype = data.meta[column] if hasattr(data, 'meta') else None)
|
|
203
|
+
|
|
204
|
+
data.set_index(index_columns, inplace = True)
|
|
205
|
+
|
|
206
|
+
self._buffer.write(struct.pack('=bxb', QTABLE, QDICTIONARY))
|
|
207
|
+
self._write(qlist(numpy.array(data_columns), qtype = QSYMBOL_LIST))
|
|
208
|
+
self._buffer.write(struct.pack('=bxi', QGENERAL_LIST, len(data_columns)))
|
|
209
|
+
for column in data_columns:
|
|
210
|
+
self._write_pandas_series(data[column], qtype = data.meta[column] if hasattr(data, 'meta') else None)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@serialize(tuple, list)
|
|
214
|
+
def _write_generic_list(self, data):
|
|
215
|
+
if self._options.pandas:
|
|
216
|
+
self._buffer.write(struct.pack('=bxi', QGENERAL_LIST, len(data)))
|
|
217
|
+
for element in data:
|
|
218
|
+
# assume nan represents a string null
|
|
219
|
+
self._write(' ' if type(element) in [float, numpy.float32, numpy.float64] and numpy.isnan(element) else element)
|
|
220
|
+
else:
|
|
221
|
+
QWriter._write_generic_list(self, data)
|