query-farm-airport-test-server 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.
- query_farm_airport_test_server/__init__.py +6 -0
- query_farm_airport_test_server/auth.py +9 -0
- query_farm_airport_test_server/py.typed +0 -0
- query_farm_airport_test_server/server.py +1799 -0
- query_farm_airport_test_server/utils.py +182 -0
- query_farm_airport_test_server-0.1.0.dist-info/METADATA +40 -0
- query_farm_airport_test_server-0.1.0.dist-info/RECORD +9 -0
- query_farm_airport_test_server-0.1.0.dist-info/WHEEL +4 -0
- query_farm_airport_test_server-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
import collections.abc
|
2
|
+
from collections.abc import Iterable, Iterator
|
3
|
+
from typing import Any, Generic, TypeVar, overload
|
4
|
+
|
5
|
+
# Type variable for the dictionary's value type
|
6
|
+
VT = TypeVar("VT")
|
7
|
+
|
8
|
+
# Type variable for the value in the fromkeys classmethod
|
9
|
+
_VT_FK_OUT = TypeVar("_VT_FK_OUT")
|
10
|
+
|
11
|
+
# Represents the type of None, for precise typing in fromkeys
|
12
|
+
NoneType = type(None)
|
13
|
+
|
14
|
+
|
15
|
+
class CaseInsensitiveDict(collections.abc.MutableMapping[str, VT], Generic[VT]):
|
16
|
+
"""
|
17
|
+
A dictionary that stores string keys in a case-insensitive manner.
|
18
|
+
|
19
|
+
The original case of the key is preserved upon first insertion or if a key
|
20
|
+
is updated with a new casing. All lookups (get, set, delete, in) are
|
21
|
+
case-insensitive with respect to string keys.
|
22
|
+
|
23
|
+
Example:
|
24
|
+
cid = CaseInsensitiveDict({'Content-Type': 'application/json'})
|
25
|
+
print(cid['content-type']) # Output: application/json
|
26
|
+
print('CONTENT-TYPE' in cid) # Output: True
|
27
|
+
|
28
|
+
cid['content-type'] = 'text/plain' # Updates value and preserved key case
|
29
|
+
print(list(cid.keys())) # Output: ['content-type']
|
30
|
+
print(cid) # Output: CaseInsensitiveDict({'content-type': 'text/plain'})
|
31
|
+
"""
|
32
|
+
|
33
|
+
# Overloads for __init__ to guide type checkers, similar to `dict`
|
34
|
+
# @overload
|
35
|
+
# def __init__(self, **kwargs: VT) -> None: ...
|
36
|
+
# @overload
|
37
|
+
# def __init__(self, map: Mapping[str, VT], **kwargs: VT) -> None: ...
|
38
|
+
# @overload
|
39
|
+
# def __init__(self, iterable: Iterable[Tuple[str, VT]], **kwargs: VT) -> None: ...
|
40
|
+
|
41
|
+
def __init__(self, *args: Any, **kwargs: VT) -> None:
|
42
|
+
"""
|
43
|
+
Initializes the CaseInsensitiveDict.
|
44
|
+
It can be initialized like a standard dict:
|
45
|
+
- CaseInsensitiveDict(mapping, **kwargs)
|
46
|
+
- CaseInsensitiveDict(iterable, **kwargs)
|
47
|
+
- CaseInsensitiveDict(**kwargs)
|
48
|
+
"""
|
49
|
+
self._store: dict[str, VT] = {}
|
50
|
+
# _key_map maps lowercase key to the actual cased key used in _store
|
51
|
+
self._key_map: dict[str, str] = {}
|
52
|
+
|
53
|
+
# Populate from args and kwargs using the update method,
|
54
|
+
# which in turn uses our __setitem__.
|
55
|
+
# dict(*args, **kwargs) creates a standard dictionary from the input,
|
56
|
+
# which self.update will then process.
|
57
|
+
self.update(dict(*args, **kwargs))
|
58
|
+
|
59
|
+
def __setitem__(self, key: str, value: VT) -> None:
|
60
|
+
"""
|
61
|
+
Set d[key] to value. Key lookups are case-insensitive.
|
62
|
+
The case of the key used in this operation is preserved.
|
63
|
+
"""
|
64
|
+
if not isinstance(key, str):
|
65
|
+
raise TypeError(f"Keys must be strings for CaseInsensitiveDict, got {type(key).__name__}")
|
66
|
+
|
67
|
+
lower_key = key.lower()
|
68
|
+
|
69
|
+
# If a key with the same lowercase form but different actual casing
|
70
|
+
# already exists, we need to remove the old entry from _store
|
71
|
+
# because _store's keys are case-sensitive.
|
72
|
+
# This ensures the new key's casing is preserved.
|
73
|
+
if lower_key in self._key_map:
|
74
|
+
original_cased_key = self._key_map[lower_key]
|
75
|
+
if original_cased_key != key:
|
76
|
+
# The new key's casing is different; remove the old one from _store.
|
77
|
+
# The value will be associated with the new key's casing.
|
78
|
+
del self._store[original_cased_key]
|
79
|
+
|
80
|
+
# Store the value with the new key (preserving its case)
|
81
|
+
self._store[key] = value
|
82
|
+
# Update the map to point to the current key's casing
|
83
|
+
self._key_map[lower_key] = key
|
84
|
+
|
85
|
+
def __getitem__(self, key: str) -> VT:
|
86
|
+
"""Return the value for key (case-insensitive lookup). Raises KeyError if not found."""
|
87
|
+
if not isinstance(key, str):
|
88
|
+
raise TypeError(f"Keys must be strings for CaseInsensitiveDict, got {type(key).__name__}")
|
89
|
+
|
90
|
+
lower_key = key.lower()
|
91
|
+
if lower_key not in self._key_map:
|
92
|
+
raise KeyError(f"Key not found: '{key}'")
|
93
|
+
|
94
|
+
# Retrieve using the original (preserved) cased key
|
95
|
+
original_cased_key = self._key_map[lower_key]
|
96
|
+
return self._store[original_cased_key]
|
97
|
+
|
98
|
+
def __delitem__(self, key: str) -> None:
|
99
|
+
"""Delete d[key]. Key lookups are case-insensitive. Raises KeyError if not found."""
|
100
|
+
if not isinstance(key, str):
|
101
|
+
raise TypeError(f"Keys must be strings for CaseInsensitiveDict, got {type(key).__name__}")
|
102
|
+
|
103
|
+
lower_key = key.lower()
|
104
|
+
if lower_key not in self._key_map:
|
105
|
+
raise KeyError(f"Key not found: '{key}'")
|
106
|
+
|
107
|
+
original_cased_key = self._key_map[lower_key]
|
108
|
+
del self._store[original_cased_key]
|
109
|
+
del self._key_map[lower_key]
|
110
|
+
|
111
|
+
def __iter__(self) -> Iterator[str]:
|
112
|
+
"""Return an iterator over the keys (with preserved casing)."""
|
113
|
+
return iter(self._store.keys())
|
114
|
+
|
115
|
+
def __len__(self) -> int:
|
116
|
+
"""Return the number of items in the dictionary."""
|
117
|
+
return len(self._store)
|
118
|
+
|
119
|
+
def __repr__(self) -> str:
|
120
|
+
"""Return a string representation of the dictionary."""
|
121
|
+
return f"{type(self).__name__}({self._store})"
|
122
|
+
|
123
|
+
def __eq__(self, other: object) -> bool:
|
124
|
+
"""
|
125
|
+
Compare this dictionary with another mapping for equality.
|
126
|
+
Comparison is case-insensitive for keys.
|
127
|
+
Both keys and values must match.
|
128
|
+
"""
|
129
|
+
if not isinstance(other, collections.abc.Mapping):
|
130
|
+
return NotImplemented
|
131
|
+
|
132
|
+
if len(self) != len(other):
|
133
|
+
return False
|
134
|
+
|
135
|
+
# Convert self to a canonical form (all string keys lowercased)
|
136
|
+
self_canonical: dict[str, VT] = {k.lower(): v for k, v in self.items()}
|
137
|
+
|
138
|
+
other_canonical: dict[str, Any] = {}
|
139
|
+
try:
|
140
|
+
for o_key, o_value in other.items():
|
141
|
+
if isinstance(o_key, str):
|
142
|
+
other_canonical[o_key.lower()] = o_value
|
143
|
+
else:
|
144
|
+
# If `other` contains any non-string key, it cannot be
|
145
|
+
# equal to this CaseInsensitiveDict, which only supports string keys.
|
146
|
+
return False
|
147
|
+
except AttributeError:
|
148
|
+
# This might happen if o_key.lower() fails unexpectedly,
|
149
|
+
# or if other.items() is problematic.
|
150
|
+
return NotImplemented
|
151
|
+
|
152
|
+
return self_canonical == other_canonical
|
153
|
+
|
154
|
+
def copy(self) -> "CaseInsensitiveDict[VT]":
|
155
|
+
"""Return a shallow copy of the dictionary."""
|
156
|
+
# The __init__ method can conveniently handle another CaseInsensitiveDict
|
157
|
+
# or any mapping to create a new instance.
|
158
|
+
return type(self)(self)
|
159
|
+
|
160
|
+
# Overloads for fromkeys to ensure correct return type inference
|
161
|
+
@overload
|
162
|
+
@classmethod
|
163
|
+
def fromkeys(cls, iterable: Iterable[str]) -> "CaseInsensitiveDict[NoneType]": ...
|
164
|
+
|
165
|
+
@overload
|
166
|
+
@classmethod
|
167
|
+
def fromkeys(cls, iterable: Iterable[str], value: _VT_FK_OUT) -> "CaseInsensitiveDict[_VT_FK_OUT]": ...
|
168
|
+
|
169
|
+
@classmethod
|
170
|
+
def fromkeys(cls, iterable: Iterable[str], value: Any = None) -> "CaseInsensitiveDict[Any]":
|
171
|
+
"""
|
172
|
+
Create a new dictionary with keys from iterable and values set to value.
|
173
|
+
If value is not specified, it defaults to None.
|
174
|
+
Keys in the iterable must be strings.
|
175
|
+
"""
|
176
|
+
instance = cls() # Creates an empty CaseInsensitiveDict
|
177
|
+
for key in iterable:
|
178
|
+
if not isinstance(key, str):
|
179
|
+
raise TypeError(f"All keys in iterable for fromkeys must be strings, got {type(key).__name__}")
|
180
|
+
# This uses the instance's __setitem__, ensuring case-insensitivity logic
|
181
|
+
instance[key] = value
|
182
|
+
return instance
|
@@ -0,0 +1,40 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: query-farm-airport-test-server
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: An Apache Arrow Flight server that is used to test the Airport extension for DuckDB.
|
5
|
+
Project-URL: Repository, https://github.com/query-farm/python-airport-test-server.git
|
6
|
+
Project-URL: Issues, https://github.com/query-farm/python-airport-test-server/issues
|
7
|
+
Author-email: Rusty Conover <rusty@query.farm>
|
8
|
+
Requires-Python: >=3.12
|
9
|
+
Requires-Dist: duckdb>=1.3.1
|
10
|
+
Requires-Dist: pyarrow>=20.0.0
|
11
|
+
Requires-Dist: query-farm-duckdb-json-serialization>=0.1.1
|
12
|
+
Requires-Dist: query-farm-flight-server
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
|
15
|
+
# query-farm-airport-test-server
|
16
|
+
|
17
|
+
**`query-farm-airport-test-server`** is a Python module that implements a lightweight in-memory Arrow Flight server for use with the [Airport DuckDB extension](https://airport.query.farm). It showcases nearly all of the Airport extension's capabilities and is designed primarily for testing and CI integration.
|
18
|
+
|
19
|
+
> ⚠️ This server is not intended as a tutorial or reference for writing Arrow Flight servers from scratch. Its purpose is to comprehensively test feature coverage, and the implementation reflects that complexity.
|
20
|
+
|
21
|
+
## Features
|
22
|
+
|
23
|
+
- In-memory storage — no persistent state
|
24
|
+
- Accepts any authentication token
|
25
|
+
- Supports full reset of data via client call
|
26
|
+
- Ideal for CI pipelines and integration tests
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
```sh
|
31
|
+
pip install query-farm-airport-test-server
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
```sh
|
37
|
+
$ airport_test_server
|
38
|
+
```
|
39
|
+
|
40
|
+
Once running, the server can be used with the test suite included in the Airport DuckDB extension.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
query_farm_airport_test_server/__init__.py,sha256=wLtpoyDnOaaaCRT0zVxXu03UlY9g5Bo04RCrmB4wclQ,72
|
2
|
+
query_farm_airport_test_server/auth.py,sha256=7ssmh_hN9WKxWwsgJnPgxNK-c_geeLqDYUo-2zHV63o,150
|
3
|
+
query_farm_airport_test_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
query_farm_airport_test_server/server.py,sha256=Q-ZCnGqfJVeVHCTEScV-sEj9MFD7VPEUmw5VfzBX5mY,71725
|
5
|
+
query_farm_airport_test_server/utils.py,sha256=-Q_jI5VnjEpVy2JUPXVqvD469-xWOpIMO1ioLk5bwbQ,7505
|
6
|
+
query_farm_airport_test_server-0.1.0.dist-info/METADATA,sha256=MxgnRGuDhgLH2yO3Y9OL5UdCjbmlr-kDCnoUBcYSsy8,1572
|
7
|
+
query_farm_airport_test_server-0.1.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
8
|
+
query_farm_airport_test_server-0.1.0.dist-info/entry_points.txt,sha256=T1BH0oQKEPQHMpatcQMSGJnAIXIm6nBs5BRtF6u-R3g,81
|
9
|
+
query_farm_airport_test_server-0.1.0.dist-info/RECORD,,
|