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 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"
@@ -0,0 +1,3 @@
1
+ """qmcp Server package"""
2
+
3
+ __version__ = "0.1.0"
@@ -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)