tableauhyperapi 0.0.19484__py3-none-macosx_13_0_arm64.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.
- tableauhyperapi/__init__.py +162 -0
- tableauhyperapi/bin/hyper/hyperd +0 -0
- tableauhyperapi/bin/libtableauhyperapi.dylib +0 -0
- tableauhyperapi/catalog.py +173 -0
- tableauhyperapi/connection.py +312 -0
- tableauhyperapi/databasename.py +85 -0
- tableauhyperapi/date.py +85 -0
- tableauhyperapi/endpoint.py +25 -0
- tableauhyperapi/hyperexception.py +128 -0
- tableauhyperapi/hyperprocess.py +155 -0
- tableauhyperapi/hyperserviceversion.py +42 -0
- tableauhyperapi/impl/__init__.py +0 -0
- tableauhyperapi/impl/cdef.py +36 -0
- tableauhyperapi/impl/cdef_compiled.py +12 -0
- tableauhyperapi/impl/converter.py +36 -0
- tableauhyperapi/impl/dll.py +18 -0
- tableauhyperapi/impl/dllutil.py +231 -0
- tableauhyperapi/impl/hapi.py +276 -0
- tableauhyperapi/impl/immutablelistwrapper.py +35 -0
- tableauhyperapi/impl/lib_h.py +358 -0
- tableauhyperapi/impl/schemaconverter.py +48 -0
- tableauhyperapi/impl/util.py +86 -0
- tableauhyperapi/impl/version.py +2 -0
- tableauhyperapi/inserter.py +719 -0
- tableauhyperapi/interval.py +220 -0
- tableauhyperapi/name.py +189 -0
- tableauhyperapi/result.py +498 -0
- tableauhyperapi/resultschema.py +93 -0
- tableauhyperapi/schemaname.py +116 -0
- tableauhyperapi/sql.py +50 -0
- tableauhyperapi/sqltype.py +346 -0
- tableauhyperapi/tabledefinition.py +254 -0
- tableauhyperapi/tablename.py +129 -0
- tableauhyperapi/timestamp.py +310 -0
- tableauhyperapi/warning.py +3 -0
- tableauhyperapi-0.0.19484.dist-info/HYPER_API_OSS_disclosure.txt +33 -0
- tableauhyperapi-0.0.19484.dist-info/LICENSE +70 -0
- tableauhyperapi-0.0.19484.dist-info/METADATA +28 -0
- tableauhyperapi-0.0.19484.dist-info/NOTICES.txt +3 -0
- tableauhyperapi-0.0.19484.dist-info/RECORD +42 -0
- tableauhyperapi-0.0.19484.dist-info/WHEEL +5 -0
- tableauhyperapi-0.0.19484.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tableauhyperapi - a Hyper client library.
|
|
3
|
+
|
|
4
|
+
This library allows spawning a local Hyper server instance, connecting to a Hyper server, running queries and commands,
|
|
5
|
+
and writing data into database tables.
|
|
6
|
+
|
|
7
|
+
tableauhyperapi classes are implemented in submodules of the tableauhyperapi package, however those submodules are
|
|
8
|
+
considered a private implementation detail, and there are no stability guarantees for them. Only symbols exported
|
|
9
|
+
from the top-level tableauhyperapi package constitute the stable public API.
|
|
10
|
+
|
|
11
|
+
Sample usage
|
|
12
|
+
------------
|
|
13
|
+
|
|
14
|
+
Create a database and push some data into it:
|
|
15
|
+
|
|
16
|
+
.. testsetup:: tableauhyperapi.__init__
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
if os.path.exists('mydb.hyper'):
|
|
20
|
+
os.remove('mydb.hyper')
|
|
21
|
+
|
|
22
|
+
.. testcode:: tableauhyperapi.__init__
|
|
23
|
+
|
|
24
|
+
from tableauhyperapi import HyperProcess, Connection, TableDefinition, SqlType, Telemetry, Inserter, CreateMode
|
|
25
|
+
|
|
26
|
+
# Start a new private local Hyper instance
|
|
27
|
+
with HyperProcess(Telemetry.SEND_USAGE_DATA_TO_TABLEAU, 'myapp') as hyper:
|
|
28
|
+
# Create the extract, replace it if it already exists
|
|
29
|
+
with Connection(hyper.endpoint, 'mydb.hyper', CreateMode.CREATE_AND_REPLACE) as connection:
|
|
30
|
+
schema = TableDefinition('foo', [
|
|
31
|
+
TableDefinition.Column('a', SqlType.text()),
|
|
32
|
+
TableDefinition.Column('b', SqlType.big_int()),
|
|
33
|
+
])
|
|
34
|
+
connection.catalog.create_table(schema)
|
|
35
|
+
with Inserter(connection, schema) as inserter:
|
|
36
|
+
inserter.add_rows([
|
|
37
|
+
['x', 1],
|
|
38
|
+
['y', 2],
|
|
39
|
+
])
|
|
40
|
+
inserter.execute()
|
|
41
|
+
|
|
42
|
+
Connect to an existing extract and append data to a table in it:
|
|
43
|
+
|
|
44
|
+
.. testsetup:: tableauhyperapi.__init__2
|
|
45
|
+
|
|
46
|
+
from tableauhyperapi import *
|
|
47
|
+
hyper = HyperProcess(Telemetry.SEND_USAGE_DATA_TO_TABLEAU, 'myapp')
|
|
48
|
+
|
|
49
|
+
.. testcode:: tableauhyperapi.__init__2
|
|
50
|
+
|
|
51
|
+
with Connection(hyper.endpoint, 'mydb.hyper') as connection:
|
|
52
|
+
with Inserter(connection, 'foo') as inserter:
|
|
53
|
+
inserter.add_row(['z', 3])
|
|
54
|
+
inserter.execute()
|
|
55
|
+
|
|
56
|
+
.. testcleanup:: tableauhyperapi.__init__2
|
|
57
|
+
|
|
58
|
+
hyper.close()
|
|
59
|
+
|
|
60
|
+
Run some queries:
|
|
61
|
+
|
|
62
|
+
.. testsetup:: tableauhyperapi.__init__3
|
|
63
|
+
|
|
64
|
+
from tableauhyperapi import *
|
|
65
|
+
hyper = HyperProcess(Telemetry.SEND_USAGE_DATA_TO_TABLEAU, 'myapp')
|
|
66
|
+
connection = Connection(hyper.endpoint, 'mydb.hyper')
|
|
67
|
+
|
|
68
|
+
.. testcode:: tableauhyperapi.__init__3
|
|
69
|
+
|
|
70
|
+
with connection.execute_query('SELECT * FROM foo') as result:
|
|
71
|
+
rows = list(result)
|
|
72
|
+
assert sorted(rows) == [['x', 1], ['y', 2], ['z', 3]]
|
|
73
|
+
|
|
74
|
+
top_a = connection.execute_scalar_query('SELECT MAX(b) FROM foo')
|
|
75
|
+
assert top_a == 3
|
|
76
|
+
|
|
77
|
+
connection.execute_command('DROP TABLE foo')
|
|
78
|
+
|
|
79
|
+
.. testcleanup:: tableauhyperapi.__init__3
|
|
80
|
+
|
|
81
|
+
connection.close()
|
|
82
|
+
hyper.close()
|
|
83
|
+
import os
|
|
84
|
+
os.remove('mydb.hyper')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
API Reference
|
|
88
|
+
-------------
|
|
89
|
+
|
|
90
|
+
.. autosummary::
|
|
91
|
+
:nosignatures:
|
|
92
|
+
|
|
93
|
+
tableauhyperapi.HyperException
|
|
94
|
+
tableauhyperapi.HyperProcess
|
|
95
|
+
tableauhyperapi.Connection
|
|
96
|
+
tableauhyperapi.Endpoint
|
|
97
|
+
tableauhyperapi.Catalog
|
|
98
|
+
tableauhyperapi.Inserter
|
|
99
|
+
tableauhyperapi.TableDefinition
|
|
100
|
+
tableauhyperapi.Persistence
|
|
101
|
+
tableauhyperapi.Result
|
|
102
|
+
tableauhyperapi.ResultSchema
|
|
103
|
+
tableauhyperapi.TypeTag
|
|
104
|
+
tableauhyperapi.SqlType
|
|
105
|
+
tableauhyperapi.Name
|
|
106
|
+
tableauhyperapi.DatabaseName
|
|
107
|
+
tableauhyperapi.SchemaName
|
|
108
|
+
tableauhyperapi.TableName
|
|
109
|
+
tableauhyperapi.Date
|
|
110
|
+
tableauhyperapi.Timestamp
|
|
111
|
+
tableauhyperapi.Interval
|
|
112
|
+
tableauhyperapi.HyperServiceVersion
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
from .catalog import Catalog
|
|
117
|
+
from .connection import Connection, CreateMode
|
|
118
|
+
from .databasename import DatabaseName
|
|
119
|
+
from .date import Date
|
|
120
|
+
from .endpoint import Endpoint
|
|
121
|
+
from .hyperexception import HyperException
|
|
122
|
+
from .hyperprocess import Telemetry, HyperProcess
|
|
123
|
+
from .hyperserviceversion import HyperServiceVersion
|
|
124
|
+
from .inserter import Inserter
|
|
125
|
+
from .interval import Interval
|
|
126
|
+
from .name import Name
|
|
127
|
+
from .result import Result
|
|
128
|
+
from .resultschema import ResultSchema
|
|
129
|
+
from .schemaname import SchemaName
|
|
130
|
+
from .sql import escape_name, escape_string_literal
|
|
131
|
+
from .sqltype import SqlType, TypeTag
|
|
132
|
+
from .tabledefinition import Nullability, Persistence, TableDefinition
|
|
133
|
+
from .tablename import TableName
|
|
134
|
+
from .timestamp import Timestamp
|
|
135
|
+
from .warning import UnclosedObjectWarning
|
|
136
|
+
|
|
137
|
+
NULLABLE = Nullability.NULLABLE
|
|
138
|
+
NOT_NULLABLE = Nullability.NOT_NULLABLE
|
|
139
|
+
PERMANENT = Persistence.PERMANENT
|
|
140
|
+
TEMPORARY = Persistence.TEMPORARY
|
|
141
|
+
|
|
142
|
+
__all__ = [
|
|
143
|
+
'HyperException',
|
|
144
|
+
'HyperProcess', 'Telemetry', 'Connection', 'Endpoint', 'CreateMode', 'Catalog',
|
|
145
|
+
'Result', 'ResultSchema', 'Inserter', 'Persistence', 'Nullability', 'SqlType', 'TypeTag',
|
|
146
|
+
'Name', 'SchemaName', 'TableName', 'DatabaseName', 'TableDefinition',
|
|
147
|
+
'escape_name', 'escape_string_literal',
|
|
148
|
+
'Date', 'Timestamp', 'Interval', 'UnclosedObjectWarning',
|
|
149
|
+
'NULLABLE', 'NOT_NULLABLE',
|
|
150
|
+
'PERMANENT', 'TEMPORARY', 'HyperServiceVersion'
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
from .impl import version
|
|
155
|
+
VERSION = version.VERSION
|
|
156
|
+
""" Version number of the library as a tuple `(major, minor, micro)`. """
|
|
157
|
+
__version__ = version.__version__
|
|
158
|
+
""" PEP-396-compliant version number of the library. """
|
|
159
|
+
except ImportError:
|
|
160
|
+
# impl/version.py is a generated file, use all zeros when running from the source directory.
|
|
161
|
+
VERSION = (0, 0, 0)
|
|
162
|
+
__version__ = '0.0.0'
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from pathlib import PurePath
|
|
2
|
+
from typing import Union, Optional, List
|
|
3
|
+
|
|
4
|
+
from .connection import Connection
|
|
5
|
+
from .tablename import Name, SchemaName, TableName, DatabaseName
|
|
6
|
+
from .tabledefinition import TableDefinition
|
|
7
|
+
from .impl import hapi
|
|
8
|
+
from .impl.dll import ffi
|
|
9
|
+
from .impl.dllutil import Error, InteropUtil, NativeTableDefinition
|
|
10
|
+
from .impl.schemaconverter import SchemaConverter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Catalog:
|
|
14
|
+
"""
|
|
15
|
+
The class which is responsible for querying and manipulating metadata.
|
|
16
|
+
|
|
17
|
+
Do not create instances of this class, use :any:`Connection.catalog` instead.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, connection: Connection):
|
|
21
|
+
self.__connection = connection
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def connection(self) -> Connection:
|
|
25
|
+
""" Gets the underlying connection. """
|
|
26
|
+
return self.__connection
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def __cdata(self):
|
|
30
|
+
return self.__connection._cdata
|
|
31
|
+
|
|
32
|
+
def has_table(self, name: Union[TableName, Name, str]) -> bool:
|
|
33
|
+
""" Does a table with this name exist? """
|
|
34
|
+
p_exists = ffi.new('bool*')
|
|
35
|
+
database_name, schema_name, table_name = TableName(name)._unescaped_triple
|
|
36
|
+
Error.check(hapi.hyper_has_table(self.__cdata,
|
|
37
|
+
InteropUtil.string_to_char_p(database_name),
|
|
38
|
+
InteropUtil.string_to_char_p(schema_name),
|
|
39
|
+
InteropUtil.string_to_char_p(table_name),
|
|
40
|
+
p_exists))
|
|
41
|
+
return p_exists[0] != 0
|
|
42
|
+
|
|
43
|
+
def get_table_definition(self, name: Union[TableName, Name, str]) -> TableDefinition:
|
|
44
|
+
""" Gets a table definition. Raises an exception if the table does not exist. """
|
|
45
|
+
database_name, schema_name, table_name = TableName(name)._unescaped_triple
|
|
46
|
+
pp_table_def = ffi.new('hyper_table_definition_t**')
|
|
47
|
+
Error.check(hapi.hyper_get_table_definition(self.__cdata,
|
|
48
|
+
InteropUtil.string_to_char_p(database_name),
|
|
49
|
+
InteropUtil.string_to_char_p(schema_name),
|
|
50
|
+
InteropUtil.string_to_char_p(table_name),
|
|
51
|
+
pp_table_def))
|
|
52
|
+
native = NativeTableDefinition(pp_table_def[0])
|
|
53
|
+
return SchemaConverter.table_definition_from_native(native)
|
|
54
|
+
|
|
55
|
+
def __create_table(self, table_definition: TableDefinition, fail_if_exists: bool):
|
|
56
|
+
native_table_def = SchemaConverter.table_definition_to_native(table_definition)
|
|
57
|
+
Error.check(hapi.hyper_create_table(self.__cdata, native_table_def.cdata, fail_if_exists))
|
|
58
|
+
|
|
59
|
+
def create_table(self, table_definition: TableDefinition):
|
|
60
|
+
"""
|
|
61
|
+
Creates a table. Raise an exception if the table already exists.
|
|
62
|
+
|
|
63
|
+
:param table_definition: the table definition.
|
|
64
|
+
"""
|
|
65
|
+
self.__create_table(table_definition, True)
|
|
66
|
+
|
|
67
|
+
def create_table_if_not_exists(self, table_definition: TableDefinition):
|
|
68
|
+
"""
|
|
69
|
+
Creates a table if it does not already exist, otherwise does nothing.
|
|
70
|
+
|
|
71
|
+
:param table_definition: the table definition.
|
|
72
|
+
"""
|
|
73
|
+
self.__create_table(table_definition, False)
|
|
74
|
+
|
|
75
|
+
def get_schema_names(self, database: Union[DatabaseName, Name, str] = None) -> List[SchemaName]:
|
|
76
|
+
"""
|
|
77
|
+
Gets the names of all schemas of the database specified by the database name, or of the first database in
|
|
78
|
+
the search path if the name is not specified.
|
|
79
|
+
"""
|
|
80
|
+
database_name = DatabaseName(database)._unescaped if database else None
|
|
81
|
+
pp_names = ffi.new('hyper_string_list_t**')
|
|
82
|
+
Error.check(hapi.hyper_get_schema_names(self.__cdata,
|
|
83
|
+
InteropUtil.string_to_char_p(database_name),
|
|
84
|
+
pp_names))
|
|
85
|
+
return [SchemaName(database, name) for name in InteropUtil.convert_and_free_string_list(pp_names)]
|
|
86
|
+
|
|
87
|
+
def get_table_names(self, schema: Union[SchemaName, Name, str]) -> List[TableName]:
|
|
88
|
+
""" Gets the names of all tables in the specified schema. """
|
|
89
|
+
database_name, schema_name = SchemaName(schema)._unescaped_double
|
|
90
|
+
pp_names = ffi.new('hyper_string_list_t**')
|
|
91
|
+
Error.check(hapi.hyper_get_table_names(self.__cdata,
|
|
92
|
+
InteropUtil.string_to_char_p(database_name),
|
|
93
|
+
InteropUtil.string_to_char_p(schema_name),
|
|
94
|
+
pp_names))
|
|
95
|
+
return [TableName(schema, name) for name in InteropUtil.convert_and_free_string_list(pp_names)]
|
|
96
|
+
|
|
97
|
+
def __create_schema(self, schema: Union[str, Name, SchemaName], fail_if_exists: bool):
|
|
98
|
+
schema = SchemaName(schema)
|
|
99
|
+
db_name, schema_name = schema._unescaped_double
|
|
100
|
+
Error.check(hapi.hyper_create_schema(self.__cdata,
|
|
101
|
+
InteropUtil.string_to_char_p(db_name),
|
|
102
|
+
InteropUtil.string_to_char_p(schema_name),
|
|
103
|
+
fail_if_exists))
|
|
104
|
+
|
|
105
|
+
def create_schema(self, schema: Union[str, Name, SchemaName]):
|
|
106
|
+
"""
|
|
107
|
+
Creates a new schema with the given name. The schema must not already exist.
|
|
108
|
+
|
|
109
|
+
:param schema: the name of the schema.
|
|
110
|
+
"""
|
|
111
|
+
self.__create_schema(schema, True)
|
|
112
|
+
|
|
113
|
+
def create_schema_if_not_exists(self, schema: Union[str, Name, SchemaName]):
|
|
114
|
+
"""
|
|
115
|
+
Creates a new schema with the given name if it does not already exist, otherwise does nothing.
|
|
116
|
+
|
|
117
|
+
:param schema: the name of the schema.
|
|
118
|
+
"""
|
|
119
|
+
self.__create_schema(schema, False)
|
|
120
|
+
|
|
121
|
+
def create_database(self, database_path: Union[str, PurePath]):
|
|
122
|
+
"""
|
|
123
|
+
Creates a new database at the given path. The file must not already exist. It does not attach the database
|
|
124
|
+
to the current connection.
|
|
125
|
+
|
|
126
|
+
:param database_path: path to the database file.
|
|
127
|
+
"""
|
|
128
|
+
Error.check(hapi.hyper_create_database(self.__cdata,
|
|
129
|
+
InteropUtil.string_to_char_p(str(database_path)),
|
|
130
|
+
True))
|
|
131
|
+
|
|
132
|
+
def create_database_if_not_exists(self, database_path: Union[str, PurePath]):
|
|
133
|
+
"""
|
|
134
|
+
Creates a new database at the given path if it does not already exist, otherwise does nothing. It does not
|
|
135
|
+
attach the database to the current connection.
|
|
136
|
+
|
|
137
|
+
Note: This method raises an exception if a file that is not a hyper database exists at the given path.
|
|
138
|
+
|
|
139
|
+
:param database_path: path to the database file.
|
|
140
|
+
"""
|
|
141
|
+
Error.check(hapi.hyper_create_database(self.__cdata,
|
|
142
|
+
InteropUtil.string_to_char_p(str(database_path)),
|
|
143
|
+
False))
|
|
144
|
+
|
|
145
|
+
def attach_database(self, database_path: Union[str, PurePath],
|
|
146
|
+
alias: Optional[Union[str, Name, DatabaseName]] = None):
|
|
147
|
+
""" Attaches a database to the underlying connection. """
|
|
148
|
+
db_path = database_path if isinstance(database_path, PurePath) else PurePath(database_path)
|
|
149
|
+
|
|
150
|
+
if not alias:
|
|
151
|
+
alias = db_path.stem
|
|
152
|
+
alias = DatabaseName(alias)
|
|
153
|
+
|
|
154
|
+
Error.check(hapi.hyper_attach_database(self.__cdata,
|
|
155
|
+
InteropUtil.string_to_char_p(str(db_path)),
|
|
156
|
+
InteropUtil.string_to_char_p(alias._unescaped)))
|
|
157
|
+
|
|
158
|
+
def detach_database(self, alias: Union[str, Name, DatabaseName]):
|
|
159
|
+
""" Detaches a database from the underlying connection. """
|
|
160
|
+
alias = DatabaseName(alias)
|
|
161
|
+
Error.check(hapi.hyper_detach_database(self.__cdata, InteropUtil.string_to_char_p(alias._unescaped)))
|
|
162
|
+
|
|
163
|
+
def detach_all_databases(self):
|
|
164
|
+
""" Detaches all databases from the underlying connection. """
|
|
165
|
+
Error.check(hapi.hyper_detach_all_databases(self.__cdata))
|
|
166
|
+
|
|
167
|
+
def drop_database(self, database_path: Union[str, PurePath]):
|
|
168
|
+
""" Drops a database file. Raise an exception if the database does not exist. """
|
|
169
|
+
Error.check(hapi.hyper_drop_database(self.__cdata, InteropUtil.string_to_char_p(str(database_path)), True))
|
|
170
|
+
|
|
171
|
+
def drop_database_if_exists(self, database_path: Union[str, PurePath]):
|
|
172
|
+
""" Drops a database file if it exists, otherwise does nothing. """
|
|
173
|
+
Error.check(hapi.hyper_drop_database(self.__cdata, InteropUtil.string_to_char_p(str(database_path)), False))
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import threading
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from pathlib import PurePath
|
|
6
|
+
from typing import Optional, Union, List, Mapping
|
|
7
|
+
|
|
8
|
+
from .endpoint import Endpoint
|
|
9
|
+
from .hyperexception import HyperException, ContextId
|
|
10
|
+
from .hyperserviceversion import HyperServiceVersion
|
|
11
|
+
from .result import Result
|
|
12
|
+
from .sqltype import NullableValue
|
|
13
|
+
from .warning import UnclosedObjectWarning
|
|
14
|
+
from .impl import hapi
|
|
15
|
+
from .impl.dll import ffi, lib
|
|
16
|
+
from .impl.dllutil import Error, Parameters, InteropUtil
|
|
17
|
+
from .impl.util import check_precondition
|
|
18
|
+
from . import catalog
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CreateMode(enum.Enum):
|
|
22
|
+
""" Constants which define what happens when connecting to a database depending on whether it already exists. """
|
|
23
|
+
|
|
24
|
+
NONE = hapi.HYPER_DO_NOT_CREATE
|
|
25
|
+
""" Do not create the database. Method will fail if database does not exist. """
|
|
26
|
+
|
|
27
|
+
CREATE = hapi.HYPER_CREATE
|
|
28
|
+
""" Create the database. Method will fail if the database already exists. """
|
|
29
|
+
|
|
30
|
+
CREATE_IF_NOT_EXISTS = hapi.HYPER_CREATE_IF_NOT_EXISTS
|
|
31
|
+
""" Create the database if it does not exist. """
|
|
32
|
+
|
|
33
|
+
CREATE_AND_REPLACE = hapi.HYPER_CREATE_AND_REPLACE
|
|
34
|
+
""" Create the database. If it already exists, drop the old one first. """
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Connection:
|
|
38
|
+
"""
|
|
39
|
+
Connects to a Hyper server.
|
|
40
|
+
|
|
41
|
+
:param endpoint: :any:`Endpoint` which specifies the Hyper instance to connect to.
|
|
42
|
+
:param database: Optional path to the database file.
|
|
43
|
+
:param create_mode: If database path is specified, defines what happens if the database already exists. By default
|
|
44
|
+
it is :any:`CreateMode.NONE`.
|
|
45
|
+
:param parameters: Optional dictionary of connection parameters to pass to Hyper.
|
|
46
|
+
The available parameters are documented
|
|
47
|
+
`in the Tableau Hyper documentation, chapter "Connection Settings"
|
|
48
|
+
<https://tableau.github.io/hyper-db/docs/hyper-api/connection#connection-settings>`__.
|
|
49
|
+
|
|
50
|
+
If the database is not specified, then it connects to the main database. This is useful to create and delete
|
|
51
|
+
databases. Note that the main database gets deleted once the :any:`HyperProcess` gets closed.
|
|
52
|
+
|
|
53
|
+
No methods of this class are thread-safe, except :any:`cancel()`, which can be called from a different thread.
|
|
54
|
+
|
|
55
|
+
.. testsetup:: connection.__init__
|
|
56
|
+
|
|
57
|
+
import os
|
|
58
|
+
from tableauhyperapi import *
|
|
59
|
+
hyper = HyperProcess(Telemetry.SEND_USAGE_DATA_TO_TABLEAU, 'myapp')
|
|
60
|
+
|
|
61
|
+
.. testcode:: connection.__init__
|
|
62
|
+
|
|
63
|
+
# Connect and create the database. If it already exists, replace it.
|
|
64
|
+
with Connection(hyper.endpoint, 'mydb.hyper', CreateMode.CREATE_AND_REPLACE) as connection:
|
|
65
|
+
schema = TableDefinition('table', [
|
|
66
|
+
TableDefinition.Column('text', SqlType.text()),
|
|
67
|
+
TableDefinition.Column('int', SqlType.int()),
|
|
68
|
+
])
|
|
69
|
+
connection.catalog.create_table(schema)
|
|
70
|
+
|
|
71
|
+
.. testcleanup:: connection.__init__
|
|
72
|
+
|
|
73
|
+
hyper.close()
|
|
74
|
+
if os.path.exists('mydb.hyper'):
|
|
75
|
+
os.remove('mydb.hyper')
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, endpoint: Endpoint,
|
|
80
|
+
database: Optional[Union[str, PurePath]] = None,
|
|
81
|
+
create_mode: Optional[CreateMode] = CreateMode.NONE,
|
|
82
|
+
parameters: Optional[Mapping[str, str]] = None):
|
|
83
|
+
self.__cdata = None
|
|
84
|
+
# Reference lib for correct gc order
|
|
85
|
+
self.__lib_ref = lib
|
|
86
|
+
|
|
87
|
+
check_precondition(isinstance(endpoint, Endpoint), "'endpoint' must be an Endpoint instance")
|
|
88
|
+
|
|
89
|
+
if isinstance(database, PurePath):
|
|
90
|
+
database = str(database)
|
|
91
|
+
|
|
92
|
+
if parameters and 'dbname' in parameters:
|
|
93
|
+
if database:
|
|
94
|
+
raise ValueError("Database name cannot be provided as a 'database' parameter in addition to setting "
|
|
95
|
+
"'dbname' in the parameters dictionary")
|
|
96
|
+
database = parameters['dbname']
|
|
97
|
+
del parameters['dbname']
|
|
98
|
+
|
|
99
|
+
self.__cdata = self.__create_connection(endpoint, database, create_mode, parameters)
|
|
100
|
+
self.__endpoint = endpoint
|
|
101
|
+
|
|
102
|
+
# Lock to serialize cancel() and close() calls.
|
|
103
|
+
self.__cancel_lock = threading.Lock()
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def __create_connection(endpoint: Endpoint,
|
|
107
|
+
database: Optional[str],
|
|
108
|
+
create_mode: CreateMode,
|
|
109
|
+
parameters: Optional[Mapping[str, str]]):
|
|
110
|
+
native_params = Parameters.create_connection_parameters()
|
|
111
|
+
native_params.set_value('endpoint', endpoint.connection_descriptor)
|
|
112
|
+
|
|
113
|
+
if endpoint.user_agent:
|
|
114
|
+
native_params.set_value('user_agent', endpoint.user_agent)
|
|
115
|
+
|
|
116
|
+
native_params.set_value('api_language', 'Python')
|
|
117
|
+
|
|
118
|
+
if database:
|
|
119
|
+
native_params.set_value('dbname', database)
|
|
120
|
+
|
|
121
|
+
if parameters:
|
|
122
|
+
for key, value in parameters.items():
|
|
123
|
+
native_params.set_value(key, value)
|
|
124
|
+
|
|
125
|
+
pp = ffi.new('hyper_connection_t**')
|
|
126
|
+
Error.check(hapi.hyper_connect(native_params.cdata, pp, create_mode.value))
|
|
127
|
+
return ffi.gc(pp[0], hapi.hyper_disconnect)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def _cdata(self):
|
|
131
|
+
if self.__cdata is None:
|
|
132
|
+
raise RuntimeError('Connection is closed')
|
|
133
|
+
return self.__cdata
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def _endpoint(self) -> Endpoint:
|
|
137
|
+
return self.__endpoint
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def is_open(self) -> bool:
|
|
141
|
+
""" Returns ``True`` if the connection has not been closed yet. """
|
|
142
|
+
return self.__cdata is not None
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def is_ready(self) -> bool:
|
|
146
|
+
""" Checks whether the connection is ready, i.e., it is not processing a query. An open :any:`Inserter` or
|
|
147
|
+
:any:`Result` keeps the connection busy. """
|
|
148
|
+
return self.__cdata is not None and hapi.hyper_connection_is_ready(self.__cdata)
|
|
149
|
+
|
|
150
|
+
def close(self):
|
|
151
|
+
""" Closes the connection. Note that this has no effect if there is an active result or data inserter.
|
|
152
|
+
These need to be closed before the connection to the server will be actually dropped."""
|
|
153
|
+
|
|
154
|
+
with self.__cancel_lock:
|
|
155
|
+
if self.__cdata is not None:
|
|
156
|
+
ffi.release(self.__cdata)
|
|
157
|
+
self.__cdata = None
|
|
158
|
+
|
|
159
|
+
def cancel(self):
|
|
160
|
+
"""
|
|
161
|
+
Cancels the current SQL command or query of this connection (if any). This method may be safely called from
|
|
162
|
+
any thread. After this method was called, the current SQL command or query may fail with a cancellation error
|
|
163
|
+
at any point during its execution. However, there are no guarantees if and when it will fail.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
with self.__cancel_lock:
|
|
167
|
+
if self.__cdata is not None:
|
|
168
|
+
try:
|
|
169
|
+
Error.check(hapi.hyper_cancel(self.__cdata))
|
|
170
|
+
except HyperException:
|
|
171
|
+
# TODO TFSID 921655: log it
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def catalog(self) -> 'catalog.Catalog':
|
|
176
|
+
""" Gets the :any:`Catalog` for this connection. """
|
|
177
|
+
return catalog.Catalog(self)
|
|
178
|
+
|
|
179
|
+
def execute_query(self, query, text_as_bytes=False) -> Result:
|
|
180
|
+
"""
|
|
181
|
+
Executes a SQL query and returns the result as a :any:`Result` object.
|
|
182
|
+
|
|
183
|
+
:param query: SQL query to execute.
|
|
184
|
+
:param text_as_bytes: optional, if ``True`` then string values read from the database will be returned as
|
|
185
|
+
UTF-8-encoded ``bytearray`` objects. By default string values are returned as ``str`` objects.
|
|
186
|
+
:return: A :any:`Result` instance. Use this method in a ``with`` statement to automatically close the result
|
|
187
|
+
when done reading from it, or call its :any:`close()<Result.close>` method. No queries can be executed or
|
|
188
|
+
tables created/opened while the result is open.
|
|
189
|
+
"""
|
|
190
|
+
pp_result = ffi.new('hyper_rowset_t**')
|
|
191
|
+
Error.check(hapi.hyper_execute_query(self._cdata,
|
|
192
|
+
InteropUtil.string_to_char_p(query),
|
|
193
|
+
pp_result))
|
|
194
|
+
return Result(text_as_bytes, self, pp_result[0])
|
|
195
|
+
|
|
196
|
+
def execute_list_query(self, query, text_as_bytes=False) -> List[List[NullableValue]]:
|
|
197
|
+
"""
|
|
198
|
+
Executes a SQL query and returns the result as list of rows of data, each represented by a list of objects.
|
|
199
|
+
|
|
200
|
+
:param query: SQL query to execute.
|
|
201
|
+
:param text_as_bytes: optional, if ``True`` then string values read from the database will be returned as
|
|
202
|
+
UTF-8-encoded ``bytearray`` objects. By default string values are returned as ``str`` objects.
|
|
203
|
+
:return: A list of rows, each represented by a list of objects. See :any:`TypeTag` documentation for how
|
|
204
|
+
database values are represented by Python objects.
|
|
205
|
+
"""
|
|
206
|
+
with self.execute_query(query, text_as_bytes) as result:
|
|
207
|
+
# Note, it is tempting to return an iterable with yield, but that would make it easy to leak the
|
|
208
|
+
# result object until it's garbage-collected (if the iteration stops in the middle).
|
|
209
|
+
return list(result)
|
|
210
|
+
|
|
211
|
+
def execute_command(self, command) -> Optional[int]:
|
|
212
|
+
"""
|
|
213
|
+
Executes a SQL statement and returns the affected row count if the statement has one.
|
|
214
|
+
|
|
215
|
+
:param command: SQL statement to execute.
|
|
216
|
+
:return: Count of affected rows if available, ``None`` otherwise.
|
|
217
|
+
"""
|
|
218
|
+
row_count_cdata = ffi.new('int*')
|
|
219
|
+
Error.check(hapi.hyper_execute_command(self._cdata,
|
|
220
|
+
InteropUtil.string_to_char_p(command),
|
|
221
|
+
row_count_cdata))
|
|
222
|
+
row_count = row_count_cdata[0]
|
|
223
|
+
if row_count < 0:
|
|
224
|
+
row_count = None
|
|
225
|
+
return row_count
|
|
226
|
+
|
|
227
|
+
def execute_scalar_query(self, query, text_as_bytes=False) -> NullableValue:
|
|
228
|
+
"""
|
|
229
|
+
Executes a scalar query, i.e. a query that returns exactly one row with one column, and returns the value
|
|
230
|
+
from the result.
|
|
231
|
+
|
|
232
|
+
:param query: SQL query to execute.
|
|
233
|
+
:param text_as_bytes: optional, if ``True`` then a string value read from the database will be returned as
|
|
234
|
+
UTF-8-encoded ``bytearray`` objects. By default string values are returned as ``str`` objects.
|
|
235
|
+
:return: the value from the result. A NULL database value is returned as ``None``. See :any:`TypeTag`
|
|
236
|
+
documentation for how database values are represented by Python objects.
|
|
237
|
+
"""
|
|
238
|
+
with self.execute_query(query, text_as_bytes) as result:
|
|
239
|
+
if len(result.schema.columns) != 1:
|
|
240
|
+
raise HyperException(ContextId(0xA1B8BBEC6D), 'Query result must have exactly one column')
|
|
241
|
+
if not result.next_row():
|
|
242
|
+
raise HyperException(ContextId(0xB8BBEC6DA1), 'Query returned zero rows')
|
|
243
|
+
value = result.get_value(0)
|
|
244
|
+
if result.next_row():
|
|
245
|
+
raise HyperException(ContextId(0xBEC6DA1B8B), 'Query returned more than one row')
|
|
246
|
+
return value
|
|
247
|
+
|
|
248
|
+
def hyper_service_version(self) -> HyperServiceVersion:
|
|
249
|
+
"""
|
|
250
|
+
Returns the Hyper Service version of this connection
|
|
251
|
+
|
|
252
|
+
:return: The Hyper Service version of this connection
|
|
253
|
+
"""
|
|
254
|
+
version = ffi.new('hyper_service_version_t*')
|
|
255
|
+
Error.check(hapi.hyper_connection_get_hyper_service_version(self._cdata, version))
|
|
256
|
+
return HyperServiceVersion(version.major, version.minor)
|
|
257
|
+
|
|
258
|
+
def is_capability_active(self, capability_flag: str) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
Returns true if the capability flag is active on this connection.
|
|
261
|
+
|
|
262
|
+
:param capability_flag: The capability flag to check. It is prefixed with `capability_`.
|
|
263
|
+
:return: true if the capability flag is active on this connection.
|
|
264
|
+
"""
|
|
265
|
+
return hapi.hyper_connection_is_capability_active(self._cdata, InteropUtil.string_to_char_p(capability_flag))
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def query_supported_hyper_service_version_range(endpoint: Endpoint) -> List[HyperServiceVersion]:
|
|
269
|
+
""" Connects to the Hyper endpoint and determines which Hyper Service version numbers are common
|
|
270
|
+
between the Hyper API and the Hyper server.
|
|
271
|
+
|
|
272
|
+
:param: endpoint Endpoint to connect to.
|
|
273
|
+
:return: List of Hyper Service versions that are supported by both this Hyper API and the endpoint.
|
|
274
|
+
"""
|
|
275
|
+
native_params = Parameters.create_connection_parameters()
|
|
276
|
+
native_params.set_value('endpoint', endpoint.connection_descriptor)
|
|
277
|
+
|
|
278
|
+
if endpoint.user_agent:
|
|
279
|
+
native_params.set_value('user_agent', endpoint.user_agent)
|
|
280
|
+
|
|
281
|
+
native_params.set_value('api_language', 'Python')
|
|
282
|
+
|
|
283
|
+
version_array_ptr = ffi.new('hyper_service_version_t**')
|
|
284
|
+
elements_cdata = ffi.new('size_t*')
|
|
285
|
+
Error.check(hapi.hyper_query_supported_hyper_service_version_range(native_params.cdata,
|
|
286
|
+
version_array_ptr,
|
|
287
|
+
elements_cdata))
|
|
288
|
+
elements = elements_cdata[0]
|
|
289
|
+
|
|
290
|
+
if version_array_ptr[0] == ffi.NULL:
|
|
291
|
+
return []
|
|
292
|
+
else:
|
|
293
|
+
list = []
|
|
294
|
+
for i in range(0, elements):
|
|
295
|
+
list.append(HyperServiceVersion(version_array_ptr[0][i].major, version_array_ptr[0][i].minor))
|
|
296
|
+
return list
|
|
297
|
+
|
|
298
|
+
def __enter__(self):
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
302
|
+
self.close()
|
|
303
|
+
|
|
304
|
+
def __del__(self):
|
|
305
|
+
if self.__cdata is not None:
|
|
306
|
+
warnings.warn('Connection has not been closed. Use Connection object in a with statement or call its '
|
|
307
|
+
'close() method when done.', UnclosedObjectWarning)
|
|
308
|
+
# it is closed by cffi, self.__cdata is a gc'ed pointer
|
|
309
|
+
|
|
310
|
+
def __repr__(self):
|
|
311
|
+
status = 'open' if self.is_ready else ('busy' if self.is_open else 'closed')
|
|
312
|
+
return f"<Connection object at {id(self):#x}; status: {status}; endpoint: {self._endpoint!r}>"
|