diff --git a/README.md b/README.md index 555c2c5..08bc1b9 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,10 @@ KX only officially supports versions of PyKX built by KX, i.e. versions of PyKX PyKX depends on the following third-party Python packages: - `pandas>=1.2, <2.0; python_version=='3.8'` -- `pandas>=1.2, <=2.3.0; python_version>'3.8'` -- `numpy~=1.22; python_version<'3.11'` -- `numpy~=1.23, <2.3.0; python_version=='3.11'` -- `numpy~=1.26, <2.3.0; python_version>'3.11'` +- `pandas>=1.2, <3.0; python_version>'3.8'` +- `numpy>=1.22; python_version<'3.11'` +- `numpy>=1.23; python_version=='3.11'` +- `numpy>=1.26; python_version>'3.11'` - `pytz>=2022.1` - `toml~=0.10.2` - `dill>=0.2.0` @@ -105,7 +105,7 @@ PyKX depends on the following third-party Python packages: They are installed automatically by `pip` when PyKX is installed. -PyKX also has an optional Python dependency of `pyarrow>=3.0.0, <19.0.0`, which can be included by installing the `pyarrow` extra, e.g. `pip install pykx[pyarrow]` +PyKX also has an optional Python dependency of `pyarrow>=3.0.0`, which can be included by installing the `pyarrow` extra, e.g. `pip install pykx[pyarrow]` When using PyKX with KX Dashboards users will be required to install `ast2json~=0.3` this can be installed using the `dashboards` extra, e.g. `pip install pykx[dashboards]` diff --git a/conda-recipe/conda_build_config.yaml b/conda-recipe/conda_build_config.yaml index 7d39e2b..b3a1c31 100644 --- a/conda-recipe/conda_build_config.yaml +++ b/conda-recipe/conda_build_config.yaml @@ -5,3 +5,4 @@ python: - 3.11 - 3.12 - 3.13 + - 3.14 diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 5b08776..7c2cd95 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -18,8 +18,10 @@ requirements: - python - setuptools>=68.0 - setuptools_scm[toml]>=8.0.0 - - cython==3.0.* - - numpy==2.*, <2.3.0 # [py==313] + - cython==3.0.* # [py<314] + - cython>=3.1.3 # [py>=314] + - numpy>=2.3.0 # [py==314] + - numpy==2.* # [py==313] - numpy==2.0.* # [py==312] - numpy==2.0.* # [py==311] - numpy==2.0.* # [py==310] @@ -28,16 +30,20 @@ requirements: - numpy==1.20.* # [py==37] - tomli>=2.0.1 - wheel>=0.36 - - sysroot_linux-64 2.17 # [linux64] - - kernel-headers_linux-64 3.10.0 # [linux64] + - sysroot_linux-64 ==2.17 # [linux and py<314] + - kernel-headers_linux-64==3.10.0 # [linux and py<314] + - sysroot_linux-64>=2.28 # [linux and py>=314] + - kernel-headers_linux-64 # [linux and py>=314] run: - python - numpy>=1.20 # [py==37] - - numpy>=1.22, <2.3.0 # [py>37] - - pandas>=1.2, <=2.3.0 # [py>38] + - numpy>=1.22 # [py>37 , py<314] + - numpy>=2.3.0 # [py==314] + - pandas>=1.2, <3.0 # [py>38 , py<314] + - pandas>=2.3.3, <3.0 # [py==314] - pandas<2.0 # [py==38] - - pyarrow>=3.0.0, <19.0.0 + - pyarrow>=3.0.0 - pytz>=2022.1 - toml>=0.10.2 - dill>=0.2.0 diff --git a/docs/extras/glossary.md b/docs/extras/glossary.md index 9b18431..a24c45c 100644 --- a/docs/extras/glossary.md +++ b/docs/extras/glossary.md @@ -74,7 +74,7 @@ A database schema is a fundamental concept in database management. It describes The `#!python sorted` attribute ensures that all items in the `#!python Vector`/`#!python Table` column are sorted in ascending order. ## Time-series analysis -Time-series analysis is a specific way of analyzing a sequence of data points collected over an interval of time. Unlike sporadic or random data collection, time-series analysis involves recording data points at consistent intervals within a set period. Use cases include finance, Internet of Things (IoT), and other domains where data evolves over time. PyKX time-series analysis uses q’s query language (qSQL) against kdb+. Learn more about [time-series models](https://code.kx.com/insights/1.9/api/machine-learning/q/analytics/api/variadic/timeseries.html). +Time-series analysis is a specific way of analyzing a sequence of data points collected over an interval of time. Unlike sporadic or random data collection, time-series analysis involves recording data points at consistent intervals within a set period. Use cases include finance, Internet of Things (IoT), and other domains where data evolves over time. PyKX time-series analysis uses q’s query language (qSQL) against kdb+. Learn more about [time-series models](https://code.kx.com/insights/api/machine-learning/q/analytics/api/variadic/timeseries.html). ## Thread A thread is an independent sequence of instructions within a program that can be executed independently of other code. Threads share the same memory space as the process they belong to, allowing them to communicate and share data efficiently. In Python, the threading module provides an intuitive API for working with threads. Learn about [PyKX calling into q from multiple threads](../examples/threaded_execution/threading.md). diff --git a/docs/getting-started/installing.md b/docs/getting-started/installing.md index b394b00..9d642de 100644 --- a/docs/getting-started/installing.md +++ b/docs/getting-started/installing.md @@ -21,7 +21,7 @@ _This page explains how to install PyKX on your machine._ Before you start, make sure you have: -- [**Python**](https://www.python.org/downloads/) (versions 3.8-3.13) +- [**Python**](https://www.python.org/downloads/) (versions 3.8-3.14) - [**pip**](https://pypi.org/project/pip/) Recommended: a virtual environment with packages such as [venv](https://docs.python.org/3/library/venv.html) from the standard library. @@ -30,9 +30,9 @@ Recommended: a virtual environment with packages such as [venv](https://docs.pyt KX only supports versions of PyKX built by KX (installed from wheel files) for: -- **Linux** (`manylinux_2_17_x86_64`, `linux-arm64`) with CPython 3.8-3.13 -- **macOS** (`macosx_10_10_x86_64`, `macosx_10_10_arm`) with CPython 3.8-3.13 -- **Windows** (`win_amd64`) with CPython 3.8-3.13 +- **Linux** (`manylinux_2_17_x86_64`, `linux-arm64`) with CPython 3.8-3.14 +- **macOS** (`macosx_10_15_x86_64`, `macosx_10_15_arm`) with CPython 3.8-3.14 +- **Windows** (`win_amd64`) with CPython 3.8-3.14 We provide assistance to user-built installations of PyKX only on a best-effort basis. @@ -87,6 +87,31 @@ You can install PyKX from three sources: ``` +=== "Install PyKX from UV" + + Ensure you have `#!bash uv` installed, instructions can be found [here.](https://docs.astral.sh/uv/getting-started/installation/) + + Then install the latest version of PyKX with the following command: + + ```sh + uv pip install pykx + + ``` + +=== "Install PyKX from pipx" + + Ensure you have `#!bash pipx` installed, installation guide can be found [here.](https://pipx.pypa.io/stable/installation/) + + Then install PyKX using the flag to install dependencies: + + ```sh + pipx install pykx --include-deps + + ``` + + This will create a virtual environment to run PyKX in, make sure you activate this to access the package. + + At this point you have [partial access to PyKX](../user-guide/advanced/modes.md#1a-running-in-unlicensed-mode). To gain access to all PyKX features, follow the steps in the next section, otherwise go straight to [3. Verify PyKX Installation](#3-verify-pykx-installation). ## 2. Install a kdb Insights license @@ -267,10 +292,10 @@ This command should display the installed version of PyKX. PyKX depends on the following third-party Python packages: - `pandas>=1.2, <2.0; python_version=='3.8'` - - `pandas>=1.2, <=2.3.0; python_version>'3.8'` - - `numpy~=1.22; python_version<'3.11'` - - `numpy~=1.23, <2.3.0; python_version=='3.11'` - - `numpy~=1.26, <2.3.0; python_version>='3.12'` + - `pandas>=1.2, <3.0; python_version>'3.8'` + - `numpy>=1.22; python_version<'3.11'` + - `numpy>=1.23; python_version=='3.11'` + - `numpy>=1.26; python_version>='3.12'` - `pytz>=2022.1` - `toml~=0.10.2` - `dill>=0.2.0` @@ -291,12 +316,10 @@ This command should display the installed version of PyKX. **Optional Python dependencies:** - - **`pyarrow >=3.0.0, <19.0.0`**: install `pyarrow` extra, for example `pip install pykx[pyarrow]`. + - **`pyarrow >=3.0.0`**: install `pyarrow` extra, for example `pip install pykx[pyarrow]`. - **`find-libpython ~=0.2`**: install `debug` extra, for example `pip install pykx[debug]`. - **`ast2json ~=0.3`**: install with `dashboards` extra, for example `pip install pykx[dashboards]` - **`dill >=0.2`**: install via pip, with `remote` extra, for example `pip install pykx[remote]` - - **`beautifulsoup4 >=4.10.0`**: install with `help` extra, for example `pip install pykx[help]` - - **`markdown2 >=2.5.0`**: install with `help` extra, for example `pip install pykx[help]` - **`psutil >=5.0.0`**: install via pip, with `streaming` extra, for example `pip install pykx[streaming]` - **`torch >2.1`**: install via pip, with `torch` extra, for example `pip install pykx[torch]` @@ -331,11 +354,11 @@ This command should display the installed version of PyKX. | Mac ARM | kdb+ 4.0 | libq.dylib | 2025.02.18 | | Mac x86 | kdb+ 4.0 | libq.dylib | 2025.02.18 | | Windows | kdb+ 4.0 | q.dll/q.lib | 2025.02.18 | -| Linux ARM | kdb+ 4.1 | libq.so | 2025.11.25 | -| Linux x86 | kdb+ 4.1 | libq.so | 2025.11.25 | -| Mac ARM | kdb+ 4.1 | libq.dylib | 2025.11.25 | -| Mac x86 | kdb+ 4.1 | libq.dylib | 2025.11.25 | -| Windows | kdb+ 4.1 | q.dll/q.lib | 2025.11.25 | +| Linux ARM | kdb+ 4.1 | libq.so | 2026.01.23 | +| Linux x86 | kdb+ 4.1 | libq.so | 2026.01.23 | +| Mac ARM | kdb+ 4.1 | libq.dylib | 2026.01.23 | +| Mac x86 | kdb+ 4.1 | libq.dylib | 2026.01.23 | +| Windows | kdb+ 4.1 | q.dll/q.lib | 2026.01.23 | | Linux ARM | Unlicensed | libe.so | 2023.11.22 | | Linux x86 | Unlicensed | libe.so | 2023.11.22 | | Mac ARM | Unlicensed | libe.so | 2023.11.22 | diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index ec3a1de..aae05bf 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -26,6 +26,12 @@ To access PyKX, import it within your Python code using the following syntax: ``` !!! Info "The use of the shortened name `#!python kx` is optional and provides a terse convention for interacting with methods and objects from the PyKX library." +Many examples in this guide make use of NumPy so we import this: + +```python +>>> import numpy as np +``` + ## 2. Generate PyKX objects You can generate PyKX objects in three ways. Click on the tabs below to follow the instructions: @@ -64,7 +70,6 @@ You can generate PyKX objects in three ways. Click on the tabs below to follow t >>> qlist pykx.LongVector(pykx.q('10 20 30')) - >>> import numpy as np >>> nplist = np.arange(0, 10, 2) >>> qlist = kx.toq(nplist) >>> qlist @@ -204,7 +209,7 @@ You can interact with PyKX objects in a variety of ways, for example, through [i ```python >>> qfunction = kx.q('{x+til 10}') - >>> qfunction(kx.toq([random() for _ in range(10)], kx.FloatVector)) + >>> qfunction(kx.FloatVector([np.random.random() for _ in range(10)])) pykx.FloatVector(pykx.q('0.3992327 1.726329 2.488636 3.653597 4.028107 5.444905 6.542917 7.00628 8.152..')) ``` @@ -218,7 +223,7 @@ You can interact with PyKX objects in a variety of ways, for example, through [i pykx.LongVector(pykx.q('5 8 3 3 10 5 3 1 9 1')) ``` -* Pass PyKX arrays of objects to Numpy functions: +* Pass PyKX arrays of objects to NumPy functions: ```python >>> qarray1 = kx.random.random(10, 1.0) @@ -242,7 +247,7 @@ You can interact with PyKX objects in a variety of ways, for example, through [i >>> N = 100 >>> qtable = kx.Table( data={ - 'x': kx.random.random(N, ['a', 'b', 'c'], + 'x': kx.random.random(N, ['a', 'b', 'c']), 'x1': kx.random.random(N, 1.0), 'x2': 5 * kx.random.random(N, 1.0), } @@ -268,7 +273,7 @@ You can interact with PyKX objects in a variety of ways, for example, through [i a 0.6212161 3.97236 .. ')) - >>> qtable.select(where=kx.Column('col1')=='a') + >>> qtable.select(where=kx.Column('x')=='a') pykx.Table(pykx.q(' x x1 x2 --------------------- @@ -320,7 +325,7 @@ You can interact with PyKX objects in a variety of ways, for example, through [i ## 4. Convert PyKX objects to Python types -To convert the objects generated via the PyKX library to the corresponding `#!python Python`, `#!python Numpy`, `#!python Pandas`, and `#!python PyArrow` types, use `#!python py`, `#!python np`, `#!python pd`, and `#!python pa` methods. Click on the tabs below to go through the examples: +To convert the objects generated via the PyKX library to the corresponding `#!python Python`, `#!python NumPy`, `#!python Pandas`, and `#!python PyArrow` types, use `#!python py`, `#!python np`, `#!python pd`, and `#!python pa` methods. Click on the tabs below to go through the examples: === "Convert to Python" @@ -334,22 +339,18 @@ To convert the objects generated via the PyKX library to the corresponding `#!py ')) >>> qdictionary.py() {'a': 5, 'b': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 'c': [0.014509072760120034, 0.9131434387527406, 0.5745006683282554, 0.9619080068077892, 0.7609488749876618]} - >>> >>> qvec = kx.toq(np.random.randint(5, size=10)) >>> qvec.py() [0, 2, 4, 1, 2, 1, 0, 1, 0, 1] ``` -=== "Convert to Numpy" +=== "Convert to NumPy" ```python - >>> import numpy as np - >>> random = np.random.random - >>> randint = np.random.randint >>> qvec = kx.q('10?5') >>> qvec.np() array([0, 2, 4, 1, 2, 1, 0, 1, 0, 1]) - >>> qtab = kx.Table([[random(), randint(0, 5)] for _ in range(5)]) + >>> qtab = kx.Table([[np.random.random(), np.random.randint(0, 5)] for _ in range(5)]) >>> qtab pykx.Table(pykx.q(' x x1 @@ -382,7 +383,7 @@ To convert the objects generated via the PyKX library to the corresponding `#!py 8 0 9 1 dtype: int64 - >>> df = pd.DataFrame(data={'x': [random() for _ in range(5)], 'x1': [randint(0, 4) for _ in range(5)]}) + >>> df = pd.DataFrame(data={'x': [np.random.random() for _ in range(5)], 'x1': [np.random.randint(0, 4) for _ in range(5)]}) >>> qtab = kx.toq(df) >>> qtab.pd() x x1 @@ -410,7 +411,7 @@ To convert the objects generated via the PyKX library to the corresponding `#!py 8 2 9 0 dtype: int64[pyarrow] - >>> df = pd.DataFrame(data={'x': [random() for _ in range(5)], 'x1': [randint(0, 4) for _ in range(5)]}) + >>> df = pd.DataFrame(data={'x': [np.random.random() for _ in range(5)], 'x1': [np.random.randint(0, 4) for _ in range(5)]}) >>> qtab = kx.toq(df) >>> qtab.pd(as_arrow=True) x x1 @@ -443,7 +444,7 @@ To convert the objects generated via the PyKX library to the corresponding `#!py 0, 1 ] - >>> df = pd.DataFrame(data={'x': [random() for _ in range(5)], 'x1': [randint(0, 4) for _ in range(5)]}) + >>> df = pd.DataFrame(data={'x': [np.random.random() for _ in range(5)], 'x1': [np.random.randint(0, 4) for _ in range(5)]}) >>> qtab = kx.toq(df) >>> qtab.pa() pyarrow.Table diff --git a/docs/help/issues.md b/docs/help/issues.md index 4ce458d..78a9f32 100644 --- a/docs/help/issues.md +++ b/docs/help/issues.md @@ -15,6 +15,8 @@ _This page details known issues and functional limitations when using PyKX eithe * Enabling the NEP-49 NumPy allocators will often segfault when running in a multiprocess setting. * The timeout value is always set to 0 when using PYKX_Q_LOCK. * Enabling PYKX_ALLOCATOR and using PyArrow tables can cause segfaults. +* Multiprocessing is not available on Windows. When using PyKX in a multiprocessing setup, PyKX must be imported and initialized within each subprocess where it is needed, regardless of the multiprocessing start method used (spawn, fork, or forkserver). +* Multithreading mode is not available on Windows. * `#!python kurl` functions require their `#!python options` dictionary to have mixed type values. Add a `#!python None` value to bypass: `#!python {'': None, ...}` * `#!python None` and `#!python pykx.Identity(pykx.q('::'))` do not pass through to single argument Python functions set under q as outlined in this example: ```python diff --git a/docs/release-notes/changelog.md b/docs/release-notes/changelog.md index 199dad9..9766bc3 100644 --- a/docs/release-notes/changelog.md +++ b/docs/release-notes/changelog.md @@ -4,6 +4,70 @@ The changelog presented here outlines changes to PyKX when operating within a Python environment specifically, if you require changelogs associated with PyKX operating under a q environment see [here](./underq-changelog.md). +# PyKX 3.1.7 + +#### Release Date + +2026-02-09 + +### Additions + +- Added support for Python 3.14. + +### Fixes and Improvements + +- Updated 4.1 to 2026.01.23 for all platforms. +- Added more strict type checks for naming `Column` objects, allowing only `str` and `SymbolAtom` values. + + === "Behavior prior to change" + + ```python + >>> kx.Column(name=0, value=[1,2,3]) + pykx.Column(name='0', value=) + + >>> kx.Column(name='a', value=(1,2,3)) + pykx.Column(name='a', value=) + ``` + + === "Behavior after change" + + ```python + >>> kx.Column(name=0, value=[1,2,3]) + TypeError: Column 'name' can only be of type Str and pykx.SymbolAtom + + >>> kx.Column(name='a', value=(1,2,3)) + pykx.Column(name='a', data=) + ``` +- Changed Pandas dependency restriction from `<= 2.3.0` to `<3.0`. +- Removed NumPy dependency restriction `<2.3.0`. +- Removed BeautifulSoup dependency from PyKX. The help system now uses native Python libraries (`urllib` and `re`) for web scraping. +- Added `.time` to `Column` objects to allow for the extraction of time information from temporal columns. + + ```python + >>> import pykx as kx + >>> tab = kx.Table(data={'time': kx.random.random(3, kx.TimestampAtom.inf)}) + >>> tab + pykx.Table(pykx.q(' + time + ----------------------------- + 2118.11.10D05:05:45.270444032 + 2052.01.18D19:21:15.973214208 + 2088.03.13D18:56:07.388233728 + ')) + >>> tab.select(kx.Column('time').time) + pykx.Table(pykx.q(' + time + ------------ + 05:05:45.270 + 19:21:15.973 + 18:56:07.388 + ')) + ``` + +### Deprecations & Removals + +- Deprecated the `value` keyword when creating `kx.Column` objects, use `data` instead. + ## PyKX 3.1.6 #### Release Date @@ -41,9 +105,9 @@ pykx.Table(pykx.q(' r_x date k_x r_y k_y -------------------------- - 2 2025.11.25 10 5 13 - 4 2025.11.26 12 6 14 - 3 2025.11.27 11 7 15 + 2 2025.11.25 10 5 13 + 4 2025.11.26 12 6 14 + 3 2025.11.27 11 7 15 ')) ``` @@ -66,7 +130,7 @@ >>> db2 = kx.DB(path="tmp/db2") PyKXWarning: Only one DB object exists at a time within a process. Use overwrite=True to overwrite your existing DB object. This warning will error in future releases. >>> db3 = kx.DB(path="tmp/db3", overwrite=True) - >>> + >>> ``` - Use of `Table.astype()` was creating a `.papi.errorList` variable, this has been moved inside the `.pykx` namespace as `.pykx.i.errorList`. @@ -100,7 +164,7 @@ >>> np.asarray(kx.DateAtom(datetime.datetime(2003, 4, 5))).dtype dtype('O') ``` - + === "Behavior post change" ```python @@ -122,7 +186,7 @@ >>> type(qtab.exec(kx.Column('col1').name('symcol'))) pykx.wrappers.SymbolVector ``` - + === "Behavior after change" ```python @@ -344,7 +408,7 @@ raise ValueError('nyi') ValueError: nyi ``` - + === "Behavior post change" ```python @@ -359,11 +423,11 @@ - Refactored `Vector.index()` from Python to q code to be more performant. - Fixed `PartitionedTable.rename_column()` operation on anymap columns. The `#` and `##` files were not renamed. Now all files are renamed. - + === "Behavior prior to change" ```python - >>> import pykx as kx + >>> import pykx as kx >>>N = 10000000 >>>trade = kx.Table(data={ ... 'date': kx.random.random(N, kx.DateAtom('today') - [1, 2, 3, 4]), @@ -396,7 +460,7 @@ === "Behavior post change" ```python - >>> import pykx as kx + >>> import pykx as kx >>>N = 10000000 >>>trade = kx.Table(data={ ... 'date': kx.random.random(N, kx.DateAtom('today') - [1, 2, 3, 4]), @@ -435,10 +499,10 @@ - Removed `Early garbage collection requires a valid q license.` error thrown on import when loading PyKX in unlicensed mode with `PYKX_GC`. - More helpful error messages in cases of missing/corrupt/incompatible licenses. - `pykx.util.add_to_config` now accepts `bool` and `int` values instead of only `str` objects. - + ```python >>> kx.util.add_to_config({'PYKX_GC': True, 'PYKX_MAX_ERROR_LENGTH': 1}) - + Configuration updated at: /home/user/.pykx-config. Profile updated: default. Successfully added: @@ -693,7 +757,7 @@ - Attempting to create a partitioned databases/add a partition to a database with a `sym_enum` keyword but no `by_field` would result in a `KeyError`. - Improved output when a licence file cannot be found, full paths checked for a license file are now shown and default license installation process is offered to user. -- Users are now given the option to input a base64 encoded string when activating an existing license +- Users are now given the option to input a base64 encoded string when activating an existing license - Using `math.inf` or `-math.inf` when creating numeric values now creates equivalent PyKX types ```python @@ -769,7 +833,7 @@ >>> kx.TimestampAtom(x) pykx.TimestampAtom(pykx.q('2025.01.28D09:43:06.000000000')) ``` - + - Fixed error when attempting to convert a `pandas.Categorical` to an existing `pykx.EnumVector` via `pykx.toq`. If the `pandas.Categorical` data contained a value outside of the `pykx.EnumVector` an error was thrown. === "Behavior prior to change" @@ -846,7 +910,7 @@ - Addition of `.replace()` function to `kx.Vector` and `kx.List` objects to search for and replace items in each collection, retaining typing where appropriate. - ```python + ```python >>> l = kx.q('("a";3;1.3;`b)') >>> l.replace(1.3, "junk") pykx.List(pykx.q(' @@ -872,7 +936,7 @@ ### Fixes and Improvements - Added warning to `kx.q.system.load` and context registration when attempting to load path with a space. Can be suppressed by enabling `PYKX_SUPPRESS_WARNINGS`. -- Changed `%%python` heading to `%%py` when calling Python code during `jupyter_qfirst` mode so as not to conflict with inbuilt Jupyter cell magics. +- Changed `%%python` heading to `%%py` when calling Python code during `jupyter_qfirst` mode so as not to conflict with inbuilt Jupyter cell magics. - Fixed `kx.license.check(format='string')` to remove newline characters during comparison. - Configuration file `.pykx-config` now supports use of boolean toml configuration @@ -914,7 +978,7 @@ columns datatypes --------------------- a "kx.LongAtom" - b "kx.IntAtom" + b "kx.IntAtom" ')) ``` @@ -954,7 +1018,7 @@ ... 'sym': kx.random.random(100, ['AAPL', 'GOOG', 'MSFT']), ... 'date': kx.random.random(100, kx.q('2022.01.01') + [0,1,2]), ... 'price': kx.random.random(100, 1000.0), - ... 'size': kx.random.random(100, 100) + ... 'size': kx.random.random(100, 100) ... }) >>> table.select(columns=kx.Column('price').max(), where=kx.Column('size') > 5) >>> table.update(column=kx.Column('price').wavg(kx.Column('size')).rename('vwap'), by=kx.Column('sym')) @@ -1141,7 +1205,7 @@ >>> import pykx as kx >>> tab = kx.q('flip (`a;`$"!";`a;`$"a b")!4 4#16?1f') >>> kx.util.detect_bad_columns(tab) - /usr/local/anaconda3/lib/python3.8/site-packages/pykx/util.py:593: UserWarning: + /usr/local/anaconda3/lib/python3.8/site-packages/pykx/util.py:593: UserWarning: Duplicate columns or columns with reserved characters detected: Duplicate columns: ['a'] Invalid columns: ['!', 'a b'] @@ -1400,17 +1464,17 @@ >>> kx.util.debug_environment() .. **** PyKX Environment Variables **** - PYKX_IGNORE_QHOME: - PYKX_KEEP_LOCAL_TIMES: - PYKX_ALLOCATOR: - PYKX_GC: - PYKX_LOAD_PYARROW_UNSAFE: - PYKX_MAX_ERROR_LENGTH: - PYKX_NOQCE: - PYKX_Q_LIB_LOCATION: - PYKX_RELEASE_GIL: - PYKX_Q_LOCK: - PYKX_DEFAULT_CONVERSION: + PYKX_IGNORE_QHOME: + PYKX_KEEP_LOCAL_TIMES: + PYKX_ALLOCATOR: + PYKX_GC: + PYKX_LOAD_PYARROW_UNSAFE: + PYKX_MAX_ERROR_LENGTH: + PYKX_NOQCE: + PYKX_Q_LIB_LOCATION: + PYKX_RELEASE_GIL: + PYKX_Q_LOCK: + PYKX_DEFAULT_CONVERSION: .. ``` @@ -1462,7 +1526,7 @@ ... 'price': kx.random.random(100, 10.0)}) >>> tab.groupby('sym')['price'].max() pykx.KeyedTable(pykx.q(' - sym| price + sym| price ---| -------- a | 9.830794 b | 9.761246 @@ -1649,7 +1713,7 @@ >>> import pykx as kx >>> db = kx.DB(path='/tmp/test directory/mydb') >>> db.tables - ['trade'] + ['trade'] ``` - Improved handling of invalid methods for Splayed and Partitioned Tables. @@ -1733,7 +1797,7 @@ ``` - Changed return of `pykx.KeyedTable.keys()` from a list of tuples to a `pykx.Table` to be consistent with `pykx.KeyedTable.values()` and unkeyed `pykx.Table`. - + === "Behavior prior to change" ```python @@ -1863,10 +1927,10 @@ >>> tab.dtypes pykx.Table(pykx.q(' - columns datatypes type + columns datatypes type ----------------------------------- a "kx.LongAtom" "kx.LongAtom" - b "kx.IntAtom" "kx.IntAtom" + b "kx.IntAtom" "kx.IntAtom" ')) >>> tab.select_dtypes(include=['SymbolAtom']) @@ -1895,10 +1959,10 @@ >>> tab.dtypes pykx.Table(pykx.q(' - columns datatypes type + columns datatypes type ----------------------------------- a "kx.LongAtom" "kx.LongAtom" - b "kx.IntAtom" "kx.IntAtom" + b "kx.IntAtom" "kx.IntAtom" ')) >>> tab.select_dtypes(include=['SymbolAtom']) @@ -1930,7 +1994,7 @@ File "pykx/_wrappers.pyx", line 486, in pykx._wrappers.factory pykx.exceptions.QError: nyi ``` - + === "Behavior post change" @@ -1957,7 +2021,7 @@ /usr/python/site-packages/pykx/wrappers.py:2246: UserWarning: Attempting to call numpy.. warn('Warning: Attempting to call numpy __array_function__ on a ' 9 - ``` + ``` === "Behavior post change" @@ -3013,7 +3077,7 @@ pykx.LongVector(pykx.q('24 28 32 36')) ``` -- Updated kdb Insights Core libraries to 4.0.8, see [here](https://code.kx.com/insights/1.8/core/release-notes/latest.html#408) for more information. +- Updated kdb Insights Core libraries to 4.0.8, see [here](https://code.kx.com/insights/core/release-notes/previous.html#408) for more information. - Updated `libq` 4.0 version to 2024.03.04 for all supported OS's. - Fix issue where use of valid C backed q `code` APIs could result in segmentation faults when called. diff --git a/docs/release-notes/deprecations.md b/docs/release-notes/deprecations.md index 268120d..e5a0bf6 100644 --- a/docs/release-notes/deprecations.md +++ b/docs/release-notes/deprecations.md @@ -1,9 +1,10 @@ # Deprecations -A list of deprecated behaviors and the version in which they were removed. +A list of deprecated behaviors and the version in which they were removed. For full release notes see [here](changelog.md), and for under q see [here](underq-changelog.md). | Feature | Alternative | Deprecated | Removed | |---------------------------------------------------|---------------------------|---------------|------------| +| `value` keyword for `Column` creation | `data` | 3.1.7 | | | Creating more than one DB object | `overwrite=True` | 3.1.5 | | | `kx.q.system.console_size` | `kx.q.system.display_size`| 3.1.3 | | | `.pykx.console[]` on Windows | | 3.1.3 | 3.1.3 | @@ -21,9 +22,11 @@ A list of deprecated behaviors and the version in which they were removed. | `PYKX_ENABLE_PANDAS_API` | | 3.0.0 | 3.0.0 | | `.pd(raw_guids)` | | 2.5.0 | 2.5.0 | -## PyKX 3.1.5 +## PyKX 3.1.7 + +- Deprecated `value` keyword when creating `Column` objects, use `data` instead -Release Date: 2025-10-21 +## PyKX 3.1.5 ### Fixes and Improvements @@ -56,14 +59,10 @@ Release Date: 2025-02-11 ## PyKX 3.0.1 -Release Date: 2024-12-04 - - Removal of column `type` from the return of `#!python dtypes` method for `#!python kx.Table` objects, previously this had raised a deprecation warning ## PyKX 3.0.0 -Release Date: 2024-11-12 - - Removal of various deprecated keywords used in table operations: - `#!python modify` keyword for `#!python select`, `#!python exec`, `#!python update` and `#!python delete` operations on `#!python pykx.Table` and `#!python pykx.KeyedTable`. This has been permanently changed to use `#!python inplace`. - `#!python replace_self` keyword when attempting to overwrite a `#!python pykx.Table` or `#!python KeyedTable` using insert/upsert functionality. This has been permanently changed to use `#!python inplace`. @@ -84,8 +83,6 @@ Release Date: 2024-11-12 ## PyKX 2.5.0 -Release Date: 2024-05-15 - - Deprecated `.pd(raw_guids)` keyword. - Renamed `labels` parameter in `Table.rename()` to `mapper` to match Pandas. Added deprecation warning to `labels`. - Deprecation of `type` column in `dtypes` output as it is a reserved keyword. Use new `datatypes` column instead. @@ -93,36 +90,26 @@ Release Date: 2024-05-15 ## PyKX 2.3.1 -Release Date: 2024-02-07 - - To align with other areas of PyKX the `upsert` and `insert` methods for PyKX tables and keyed tables now support the keyword argument `inplace`, this change will deprecate usage of `replace_self` with the next major release of PyKX. ## PyKX 2.2.1 -Release Date: 2023-11-30 - - Deprecation of internally used environment variable `UNDER_PYTHON` which has been replaced by `PYKX_UNDER_PYTHON` to align with other internally used environment variables. - Addition of deprecation warning for environmental configuration option `PYKX_NO_SIGINT` which is to be replaced by `PYKX_NO_SIGNAL`. This is used when users require no signal handling logic overwrites and now covers `SIGTERM`, `SIGINT`, `SIGABRT` signals amongst others. ## PyKX 1.6.1 -Release Date: 2023-07-19 - - Added deprecation warning around the discontinuing of support for Python 3.7. ## PyKX 1.0.1 -Release Date: 2022-03-18 - - The `sync` parameter for `pykx.QConnection` and `pykx.QConnection.__call__` has been renamed to the less confusing name `wait`. The `sync` parameter remains, but its usage will result in a `DeprecationWarning` being emitted. The `sync` parameter will be removed in a future version. ## PyKX 1.0.0 -Release Date: 2022-02-14 - - The `pykdb.q.ipc` attribute has been removed. The IPC module can be accessed directly instead at `pykx.ipc`, but generally one will only need to access the `QConnection` class, which can be accessed at the top-level: `pykx.QConnection`. - The `pykdb.q.K` attribute has been removed. Instead, `K` types can be used as constructors for that type by leveraging the `toq` module. For example, instead of `pykdb.q.K(x)` one should write `pykx.K(x)`. Instead of `pykx.q.K(x, k_type=pykx.k.SymbolAtom)` one should write `pykx.SymbolAtom(x)` or `pykx.toq(x, ktype=pykx.SymbolAtom)`. - Most `KdbError`/`QError` subclasses have been removed, as identifying them is error prone, and we are unable to provide helpful error messages for most of them. diff --git a/docs/user-guide/advanced/threading.md b/docs/user-guide/advanced/threading.md index 2d53e04..3201f02 100644 --- a/docs/user-guide/advanced/threading.md +++ b/docs/user-guide/advanced/threading.md @@ -27,6 +27,7 @@ All calls to q from other threads are run on this background thread, created usi Before globally enabling this functionality, consider the following: +- **Platform Support**: Multithreading mode is only supported on Linux and macOS; it is not available on Windows. - **Concurrency Cost**: While the overhead for offloading calls onto a secondary thread is low, there will always be a cost in forcing a thread context switch. As such single-threaded performance is faster at the cost of concurrency. - **Memory-Safe Use**: While using `#!python PYKX_THREADING` it's not possible nor memory safe to have `#!python q` call back into Python; this could result in memory corruption or side-effects which may not be immediately obvious. - **Shutdown**: When using `#!python PYKX_THREADING`, it creates a background thread for running queries to `#!python q`. Make sure to call `#!python kx.shutdown_thread()` at the end of your script to properly close this thread. If you don’t, the thread will remain running in the background after the script finishes. To avoid this, it’s best to start your `#!python main` function within a `#!python try` - `#!python finally` block. diff --git a/docs/user-guide/fundamentals/query/perf.md b/docs/user-guide/fundamentals/query/perf.md index 688a0e6..16ad99e 100644 --- a/docs/user-guide/fundamentals/query/perf.md +++ b/docs/user-guide/fundamentals/query/perf.md @@ -81,7 +81,7 @@ The following shows the scaling of queries based on the number of rows on an in- ## SQL -To optimize frequently called SQL queries you can make use of the [prepare](../../../api/query.md#pykx.query.SQL.prepare) and [execute](../../../api/query.md#pykx.query.SQL.execute) functionality to separate SQL parsing from query execution as detailed [here](https://code.kx.com/insights/1.10/core/sql.html#prepare-and-execute). +To optimize frequently called SQL queries you can make use of the [prepare](../../../api/query.md#pykx.query.SQL.prepare) and [execute](../../../api/query.md#pykx.query.SQL.execute) functionality to separate SQL parsing from query execution as detailed [here](https://code.kx.com/insights/core/sql.html#prepare-and-execute). ## Next Steps diff --git a/docs/user-guide/fundamentals/query/sql.md b/docs/user-guide/fundamentals/query/sql.md index 020da72..5386d6a 100644 --- a/docs/user-guide/fundamentals/query/sql.md +++ b/docs/user-guide/fundamentals/query/sql.md @@ -121,7 +121,7 @@ GOOG 2022.01.02 423.6121 Now that you have learnt the fundamentals of how to query your data using the SQL API you may be interested in: -- To optimize frequently called SQL queries the [prepare](../../../api/query.md#pykx.query.SQL.prepare) and [execute](../../../api/query.md#pykx.query.SQL.execute) methods can be used to separate SQL parsing from query execution as detailed [here](https://code.kx.com/insights/1.10/core/sql.html#prepare-and-execute). +- To optimize frequently called SQL queries the [prepare](../../../api/query.md#pykx.query.SQL.prepare) and [execute](../../../api/query.md#pykx.query.SQL.execute) methods can be used to separate SQL parsing from query execution as detailed [here](https://code.kx.com/insights/core/sql.html#prepare-and-execute). - If you want to query your data in a more Python-first way follow the guide [here](./pyquery.md). - If you want to query your data in q follow the guide [here](./qquery.md). - To learn how to make your queries more performant following the tips and tricks [here](./perf.md). diff --git a/mkdocs.yml b/mkdocs.yml index 293a4d2..ef490ea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ site_name: 'PyKX' site_author: 'KX' site_description: 'PyKX Documentation' site_url: 'https://code.kx.com/pykx' -copyright: '© 2025 Kx Systems, Inc. KX and kdb+ are registered trademarks of Kx Systems, Inc., a subsidiary of FD Technologies plc.' +copyright: '© 2026 KX Systems, Inc. KX, KDB-X, and kdb+ are registered trademarks of KX Systems, Inc., a subsidiary of KX Software Limited.' site_dir: 'public' dev_addr: 'localhost:8080' @@ -104,7 +104,7 @@ plugins: rendering: docstring_section_style: 'table' heading_level: 2 - line_length: 80 + line_length: 100 members_order: 'source' merge_init_into_class: true separate_signature: true @@ -184,8 +184,8 @@ nav: - Home: 'https://code.kx.com/insights/' - kdb+ and q: 'https://code.kx.com/q' - kdb Insights SDK: "https://code.kx.com/insights/core" - - kdb Insights Enterprise: "https://code.kx.com/insights/platform/" - - KDB.AI: "https://code.kx.com/kdbai/" + - kdb Insights Enterprise: "https://docs.kx.com/insights-enterprise-latest/insights/enterprise/home.htm" + - KDB.AI: "https://code.kx.com/kdbai/latest/" - PyKX: - index.md - Get Started: @@ -283,7 +283,7 @@ nav: - PyKX as a server: examples/server/server.md - Real-time streaming: examples/streaming/index.md - Multithreaded execution: examples/threaded_execution/threading.md - - Asynchronous Queries: examples/AsynchronousQueries/async_querying.md + - Asyncronous Queries: examples/AsynchronousQueries/async_querying.md - Releases: - Release notes: - PyKX: release-notes/changelog.md @@ -301,5 +301,5 @@ nav: - PyKX license: license.md - Contributors: contributors.md - APIs: 'https://code.kx.com/insights/api/index.html' - - Licensing: 'https://code.kx.com/insights/licensing/licensing/' + - Licensing: 'https://code.kx.com/insights/licensing/index.html' - Help: 'https://code.kx.com/home/support.html' diff --git a/pyproject.toml b/pyproject.toml index ade9e55..567110c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,11 +52,13 @@ dependencies = [ "numpy~=1.22; python_version=='3.8'", "numpy~=1.22; python_version=='3.9'", "numpy>=1.22; python_version=='3.10'", - "numpy>=1.23, < 2.3.0; python_version=='3.11'", - "numpy>=1.26, < 2.3.0; python_version=='3.12'", - "numpy>=1.26, < 2.3.0; python_version=='3.13'", - "pandas>=1.2, < 2.0; python_version=='3.8'", - "pandas>=1.2, <= 2.3.0; python_version>'3.8'", + "numpy>=1.23; python_version=='3.11'", + "numpy>=1.26; python_version=='3.12'", + "numpy>=1.26; python_version=='3.13'", + "numpy>=2.3.0; python_version=='3.14'", + "pandas>=1.2, <2.0.0; python_version=='3.8'", + "pandas>=1.2, <3.0; python_version>'3.8' and python_version<'3.14'", + "pandas>= 2.3.3, <3.0; python_version=='3.14'", "pytz>=2022.1", "toml~=0.10.2", "dill>=0.2.0", @@ -101,7 +103,7 @@ lint = [ "pyproject-flake8==0.0.1a2", ] pyarrow = [ - "pyarrow>=3.0.0, <19.0.0", + "pyarrow>=3.0.0", ] streaming = [ "psutil>=5.0.0" @@ -118,7 +120,8 @@ torch = [ test = [ "coverage[toml]==6.3.2", "Cython~=3.0.0", - "pytest==7.1.2", + "pytest==7.1.2; python_version<'3.14'", + "pytest==8.4.0; python_version=='3.14'", "pytest-asyncio==0.18.3", "pytest-benchmark==3.4.1", "pytest-cov==3.0.0", @@ -130,10 +133,6 @@ test = [ "pytest-timeout>=2.0.0", "IPython", ] -help = [ - "beautifulsoup4==4.10.0", - "markdown2==2.5.0", -] [project.urls] @@ -150,9 +149,13 @@ requires = [ "numpy~=1.22.0; python_version=='3.8'", # Use the highest patch version of numpy 1.22.x, this will still support a user using numpy version 1.22.0 "numpy~=2.0; python_version=='3.9'", "numpy~=2.0; python_version=='3.10'", - "numpy~=2.0, < 2.3.0; python_version=='3.11'", - "numpy~=2.0, < 2.3.0; python_version=='3.12'", - "numpy~=2.0, < 2.3.0; python_version=='3.13'", + "numpy~=2.0; python_version=='3.11'", + "numpy~=2.0; python_version=='3.12'", + "numpy~=2.0; python_version=='3.13'", + "numpy>=2.3.0; python_version=='3.14'", + "numpy~=2.0; python_version=='3.11'", + "numpy~=2.0; python_version=='3.12'", + "numpy~=2.0; python_version=='3.13'", "setuptools~=68.0.0; python_version=='3.7'", "setuptools~=69.0.2; python_version!='3.7'", "setuptools-scm[toml]~=7.1.0; python_version=='3.7'", diff --git a/src/pykx/_wrappers.pyx b/src/pykx/_wrappers.pyx index 80bfa99..a0913ce 100644 --- a/src/pykx/_wrappers.pyx +++ b/src/pykx/_wrappers.pyx @@ -546,11 +546,11 @@ cdef inline factory(uintptr_t addr, bint incref, str name='', bint err_preamble= err_string = str((addr).s, 'utf-8') if err_string == 'nosocket': err_string = 'nosocket: Cannot open or use a socket on a thread other than main.\n'\ - 'Read https://code.kx.com/user-guide/advanced/threading.html for more information' + 'Read https://code.kx.com/pykx/user-guide/advanced/threading.html for more information' elif err_string == 'noupdate': err_string = 'noupdate: Cannot update a global variable while using:\n\t- Multithreaded mode'\ '\n\t- peach with secondary threads\n\t- `-b` command line argument or reval code.\n'\ - 'Read https://code.kx.com/user-guide/advanced/threading.html for more information' + 'Read https://code.kx.com/pykx/user-guide/advanced/threading.html for more information' elif err_string == 'par': err_string = 'par: Cannot execute an unsupported operation on a partitioned table or its '\ 'constituent parts' diff --git a/src/pykx/config.py b/src/pykx/config.py index b166cad..9bd625b 100644 --- a/src/pykx/config.py +++ b/src/pykx/config.py @@ -326,7 +326,7 @@ def _license_install(intro=None, return_value=False, license_check=False, licens except BaseException: raise Exception('Unable to open web browser') - install_type = input('\nPlease select the method you wish to use to activate your' + install_type = input('\nPlease select the method you wish to use to activate your ' 'license:\n [1] Download the license file provided in your ' 'welcome email and input the file path (Default)' '\n [2] Input the activation key (base64 encoded string) ' diff --git a/src/pykx/help.py b/src/pykx/help.py index e77b0b0..9163713 100644 --- a/src/pykx/help.py +++ b/src/pykx/help.py @@ -1,9 +1,14 @@ -import importlib.util -import warnings import os +import re +from urllib.request import Request, urlopen +from urllib.error import HTTPError, URLError __all__ = ["help"] -_filepath = os.path.join(os.path.dirname(__file__), 'docs', 'api', 'pykx-execution', 'q.md') +_filepath = os.path.join( + os.path.dirname(__file__), '..', '..', 'docs', 'api', + 'pykx-execution', 'q.md' +) +_base_url = "https://code.kx.com/pykx/4.0/api/pykx-execution/q.html" def __dir__(): @@ -15,101 +20,195 @@ def _init(_q): q = _q +def _fetch_web_documentation(keyword): + """Fetch documentation from the PyKX website.""" + url = _base_url + try: + req = Request(url, headers={'User-Agent': 'PyKX/help'}) + with urlopen(req, timeout=5) as response: + html = response.read().decode('utf-8') + + # Extract the article content + article_match = re.search( + r']*class="[^"]*md-content__inner[^"]*"[^>]*>' + r'(.*?)', + html, re.DOTALL + ) + if not article_match: + return None + + article_html = article_match.group(1) + + # Find the h3 section for this keyword + # Pattern:

...

... content ...

+ pattern = rf']*id="{keyword}"[^>]*>.*?

(.*?)(?=]*id="|$)' + section_match = re.search(pattern, article_html, re.DOTALL) + + if not section_match: + return None + + section_html = section_match.group(1) + text = f" • {keyword}\n\n" + + # Extract paragraphs + for para_match in re.finditer(r']*)?>(.*?)

', section_html, re.DOTALL): + para_text = re.sub(r'<[^>]+>', '', para_match.group(1)) + para_text = _decode_html_entities(para_text).strip() + if para_text: + text += para_text + "\n\n" + + # Extract code blocks + for code_match in re.finditer(r']*>]*>(.*?)', + section_html, re.DOTALL): + code_text = _decode_html_entities(code_match.group(1)).strip() + if code_text: + text += "\n" + for line in code_text.split("\n"): + text += " " + line + "\n" + text += "\n" + + return text + + except (URLError, HTTPError, TimeoutError): + return None + except Exception: + return None + + +def _decode_html_entities(text): + """Decode common HTML entities.""" + return (text.replace('<', '<') + .replace('>', '>') + .replace('&', '&') + .replace('"', '"') + .replace(''', "'") + .replace(' ', ' ') + .replace('¶', '') # Remove paragraph symbols + .replace('·', '·')) + + def _load_markdown_section(keyword, file_path=_filepath): - with open(file_path, 'r') as f: - lines = f.readlines() + """Load and parse markdown section for a keyword.""" + try: + with open(file_path, 'r') as f: + lines = f.readlines() + + in_section = False + section_lines = [] + for line in lines: + if line.startswith(f'### [{keyword}]'): + in_section = True + section_lines.append(line) + elif line.startswith('###') and in_section: + break + elif in_section: + section_lines.append(line) - # Extract the specific section - in_section = False - section_lines = [] - for line in lines: - if line.startswith(f'### [{keyword}]'): - in_section = True - section_lines.append(line) - elif line.startswith('###') and in_section: - break - elif in_section: - section_lines.append(line) - - if not section_lines: - return "No description available for this function." - - # Convert the section to HTML - section_markdown = ''.join(section_lines) - - md2_spec = importlib.util.find_spec("markdown2") - if md2_spec is None: - warnings.warn("The 'markdown2' package is required to use the 'qhelp' function.") - return "" - else: - import markdown2 - section_html = markdown2.markdown(section_markdown, extras=["fenced-code-blocks"]) + if not section_lines: + return None - return section_html + return section_lines + except IOError: + return None def qhelp(keyword): """Parse and format the documentation for a given keyword.""" - bs4_spec = importlib.util.find_spec("bs4") - if bs4_spec is None: - warnings.warn("The 'bs4' package is required to use the 'qhelp' function.") - return "" - else: - from bs4 import BeautifulSoup + markdown_lines = _load_markdown_section(keyword) + if markdown_lines: + text = f" • {keyword}\n\n" + + in_code_block = False + table_lines = [] + in_table = False + + for i, line in enumerate(markdown_lines): + if i == 0: + continue + + if line.strip().startswith('```'): + if in_code_block: + in_code_block = False + text += "\n" + else: + in_code_block = True + text += "\n" + continue + + if in_code_block: + text += " " + line.rstrip() + "\n" + continue + + if '|' in line and not line.strip().startswith('#'): + if not in_table: + in_table = True + table_lines = [] + table_lines.append(line) + continue + elif in_table: + if table_lines: + text += _parse_markdown_table(table_lines) + in_table = False + table_lines = [] + + if line.strip() == '': + if text.endswith('\n\n'): + continue + text += "\n" + continue - html_content = _load_markdown_section(keyword) - if html_content: - soup = BeautifulSoup(html_content, "html.parser") - tag = soup.find("h3", string=keyword) + if line.strip().startswith('- ') or line.strip().startswith('* '): + list_text = line.strip()[2:] + list_text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', list_text) + text += " - " + list_text + "\n" + continue - if tag is None: - return None + if not line.startswith('#'): + line_text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', line.strip()) + line_text = re.sub(r'`([^`]+)`', r'\1', line_text) + if line_text: + text += line_text + "\n" - text = " • " + tag.text + "\n\n" - for x in tag.findAllNext(): - if x.name == "h2": - break + if in_table and table_lines: + text += _parse_markdown_table(table_lines) - elif x.name == "h3": - break + return text - elif x.name == "p": - only_link = True - for w in x: - if w.name != "a": - only_link = False - if not only_link: - text += x.text + "\n" + # Fallback to web scraping if local file fails + web_result = _fetch_web_documentation(keyword) + if web_result: + return web_result - elif x.name == "pre": - text += "\n" - for line in x.text.split("\n"): - text += " " + line + "\n" - - elif x.name == "li": - text += " - " + x.text + "\n" - - elif x.name == "thead": - table = [] - tmp = [] - for cell in x.text.split("\n"): - if cell != "": - tmp += [cell] - table += [tmp] - - elif x.name == "tbody": - for row in x.text.split("\n\n\n"): - tmp = [] - for cell in row.split("\n"): - if cell != "": - tmp += [cell] - table += [tmp] - text += __ascii_table(table[1:], border=True, header=table[0]) + "\n" + # If both local and web sources fail + return (f" • {keyword}\n\n" + f"Documentation not available. Please check your internet connection\n" + f"or visit {_base_url}#{keyword} for documentation.") - return text - else: - print("html error") - return None + +def _parse_markdown_table(lines): + """Parse markdown table into ASCII table format.""" + if not lines: + return "" + + rows = [] + header = None + + for line in lines: + if re.match(r'^\s*\|[\s\-:|]+\|\s*$', line): + continue + + cells = [cell.strip() for cell in line.split('|')] + cells = [c for c in cells if c] + + if cells: + if header is None: + header = cells + else: + rows.append(cells) + + if header and rows: + return __ascii_table(rows, border=True, header=header) + "\n" + return "" def __ascii_table(table: list, header: list = None, align='left', border=False): diff --git a/src/pykx/lib/4-1-libs/q.k b/src/pykx/lib/4-1-libs/q.k index 68d1d3e..74d1eb4 100644 --- a/src/pykx/lib/4-1-libs/q.k +++ b/src/pykx/lib/4-1-libs/q.k @@ -98,7 +98,7 @@ j12:36/:nA?;x12:nA@0x24\: /base36 J from char12(cusip) btoa:-32!;atob:-44!;sha1:-33!;prf0:+`name`file`line`col`text`pos!*-37! objp:{x like"[mgs][s3]://*"}; lo:{if[$[1>@d:!f:-1!x;1;`.d~*d];:.[$[qt d;*|`\:f;99=@d;`.;'`type];();:;d:. f]];d@:&~d like"*$";p:(d=`par.txt)|d like"[0-9]*"; - if[y;if[objp x;'"no cd to object storage"];."\\cd ",$x;f:`:.];{.q.set[*|`\:x]$[0h>@!x;. x;x`]}'`/:'f,'d@&~(d=`html)|(d like"*#")|p|s:d like"*.?";if[|/p;L[d@&p;f;~y|objp x]];if[z&~`.=x;(."\\l ",$:)'`/:'f,'d@&s&~p];} + if[y;if[objp x;'"no cd to object storage"];."\\cd ",$x;f:`:.];{.q.set[*|`\:x]$[0h>@!x;. x;x`]}'`/:'f,'d@&~(d=`html)|(d like"*#")|p|s:|/d like/:("*.[kq]_";"*.?");if[|/p;L[d@&p;f;~y|objp x]];if[z&~`.=x;(."\\l ",$:)'`/:'f,'d@&s&~p];} l:lo[;1;1] sw:{.q.sublist[_.5**|."\\C"]x};tab:{" "/:'(x*|/#:''y)$/:y};t0:{$[x>#y;((#*y)#'" -"),y;y]} diff --git a/src/pykx/q.so/libs/4-1/l64/libq.so b/src/pykx/q.so/libs/4-1/l64/libq.so index 9402e6a..48ff188 100755 Binary files a/src/pykx/q.so/libs/4-1/l64/libq.so and b/src/pykx/q.so/libs/4-1/l64/libq.so differ diff --git a/src/pykx/q.so/libs/4-1/l64arm/libq.so b/src/pykx/q.so/libs/4-1/l64arm/libq.so index 0fa82bc..3ea98ee 100755 Binary files a/src/pykx/q.so/libs/4-1/l64arm/libq.so and b/src/pykx/q.so/libs/4-1/l64arm/libq.so differ diff --git a/src/pykx/q.so/libs/4-1/m64/libq.dylib b/src/pykx/q.so/libs/4-1/m64/libq.dylib index 548949f..5d1f4c9 100755 Binary files a/src/pykx/q.so/libs/4-1/m64/libq.dylib and b/src/pykx/q.so/libs/4-1/m64/libq.dylib differ diff --git a/src/pykx/q.so/libs/4-1/m64arm/libq.dylib b/src/pykx/q.so/libs/4-1/m64arm/libq.dylib index 4088297..86b67a2 100755 Binary files a/src/pykx/q.so/libs/4-1/m64arm/libq.dylib and b/src/pykx/q.so/libs/4-1/m64arm/libq.dylib differ diff --git a/src/pykx/q.so/libs/4-1/w64/q.dll b/src/pykx/q.so/libs/4-1/w64/q.dll index c679126..eeee7f1 100644 Binary files a/src/pykx/q.so/libs/4-1/w64/q.dll and b/src/pykx/q.so/libs/4-1/w64/q.dll differ diff --git a/src/pykx/query.py b/src/pykx/query.py index 5a6e23e..8ed61f6 100644 --- a/src/pykx/query.py +++ b/src/pykx/query.py @@ -38,7 +38,7 @@ class QSQL: ```python qtable = pykx.q('([]1 2 3;4 5 6)') - qtable.update(pykx.Column('x', value=[10, 20, 30])) + qtable.update(pykx.Column('x', data=[10, 20, 30])) ``` 3. Development and maintenance of this interface is easier with regard to the different @@ -246,13 +246,13 @@ def update(self, ```python qtab.update({'eye': '`blue`brown`green'}) - qtab.update(pykx.Column('age', value=[25, 30, 31])) + qtab.update(pykx.Column('age', data=[25, 30, 31])) ``` Update the content of a column restricting scope using a where clause ```python - qtab.update(pykx.Column('age', value=['blue']), where=pykx.Column('hair')=='fair') + qtab.update(pykx.Column('age', data=['blue']), where=pykx.Column('hair')=='fair') ``` Define a q table suitable for by clause example @@ -424,17 +424,17 @@ def _generate_clause_columns_by(self, clause_value, clause_name, query_type): elif isinstance(clause_value, k.Column): if query_type == 'exec': if clause_value._is_tree: - return [b'.[;enlist 0;eval]', clause_value._value] + return [b'.[;enlist 0;eval]', clause_value._data] else: - return k.ParseTree(clause_value._value) + return k.ParseTree(clause_value._data) elif query_type == 'delete' and clause_name == 'columns': return [b'enlist', clause_value._name] else: if clause_value._is_tree: return [b'{enlist[x]!enlist .[y;enlist 0;eval]}', - clause_value._name, clause_value._value] + clause_value._name, clause_value._data] else: - return {clause_value._name: clause_value._value} + return {clause_value._name: clause_value._data} elif isinstance(clause_value, k.Variable): if query_type == 'exec': return k.ParseTree(clause_value._name) @@ -459,7 +459,7 @@ def _generate_clause_columns_by(self, clause_value, clause_name, query_type): for x in clause_value: if isinstance(x, k.Column): kys.append(x._name) - vls.append(x._value) + vls.append(x._data) else: kys.append(x) vls.append(x) @@ -480,9 +480,9 @@ def _generate_clause_columns_by_dict(self, clause_value): clause_dict[key] = [1, k.CharVector(val)] elif isinstance(val, k.Column): if val._is_tree: - clause_dict[key] = [2, val._value] + clause_dict[key] = [2, val._data] else: - clause_dict[key] = [0, val._value] + clause_dict[key] = [0, val._data] else: clause_dict[key] = [0, val] return [b'''{ diff --git a/src/pykx/toq.pyx b/src/pykx/toq.pyx index a9ef043..ef606cf 100644 --- a/src/pykx/toq.pyx +++ b/src/pykx/toq.pyx @@ -2013,7 +2013,10 @@ def from_arrow_py(x, """ if pa is None: raise PyArrowUnavailable - return toq(x.as_py(), ktype=ktype, cast=cast, handle_nulls=handle_nulls) + val = x.as_py() + if isinstance(x, pa.lib.HalfFloatScalar) and val is not None: + val = np.float16(val) + return toq(val, ktype=ktype, cast=cast, handle_nulls=handle_nulls) def from_datetime_date(x: Any, @@ -2994,7 +2997,7 @@ class ToqModule(ModuleType): elif isinstance(x, k.GroupbyTable): return self(x.tab, ktype=ktype, cast=cast, handle_nulls=handle_nulls) elif isinstance(x, k.Column): - return self(x._value) + return self(x._data) elif isinstance(x, k.QueryPhrase): return self(x._phrase) elif isinstance(x, k.Variable): diff --git a/src/pykx/wrappers.py b/src/pykx/wrappers.py index 1d44d04..0459f53 100644 --- a/src/pykx/wrappers.py +++ b/src/pykx/wrappers.py @@ -1924,10 +1924,18 @@ def pa(self, *, raw: bool = False, has_nulls: Optional[bool] = None): np_array = self.np(raw=raw, has_nulls=has_nulls) if not raw and isinstance(self, List): null_inds = [] + atom_types = set() for i in range(len(self)): - if isinstance(self._unlicensed_getitem(i), IntegralNumericAtom)\ - and self._unlicensed_getitem(i).is_null: + item = self._unlicensed_getitem(i) + if not isinstance(item, Function): + atom_types.add(type(item)) + if isinstance(item, IntegralNumericAtom) and item.is_null: null_inds.append(i) + if len(atom_types) > 1: + raise QError( + 'Unable to convert pykx.List with non conforming types ' + 'to PyArrow,\n failed with error: ' + 'cannot mix types in a pykx.List for PyArrow conversion') if 0 != len(null_inds): np_array[null_inds] = None return pa.array(np_array) @@ -2620,6 +2628,11 @@ def py(self, *, raw: bool = False, has_nulls: Optional[bool] = None, stdlib: boo def np(self, *, raw: bool = False, has_nulls: Optional[bool] = None): return _wrappers.guid_vector_np(self, raw, has_nulls) + def __arrow_array__(self, type=None): + if pa is None: + raise PyArrowUnavailable # nocov + return pa.array(self.np(raw=True).view('S16'), pa.binary(16)) + def pa(self, *, raw: bool = False, has_nulls: Optional[bool] = None): if pa is None: raise PyArrowUnavailable # nocov @@ -5184,10 +5197,7 @@ def each_left(self): vs = each_left -bs4_spec = importlib.util.find_spec("bs4") -md2_spec = importlib.util.find_spec("markdown2") -if bs4_spec is not None and md2_spec is not None: - Function.scan.__doc__ = help.qhelp('scan') +Function.scan.__doc__ = help.qhelp('scan') class Lambda(Function): @@ -5759,7 +5769,7 @@ def __init__(self, tree): if isinstance(tree, str): tree = q.parse(CharVector(tree)) elif isinstance(tree, Column): - tree = tree._value + tree = tree._data elif isinstance(tree, QueryPhrase): tree = tree._phrase elif isinstance(tree, Variable): @@ -5869,7 +5879,7 @@ def exists(self): class Column: """Helper class creating queries for the Query API""" - def __init__(self, column=None, name=None, value=None, is_tree=False): + def __init__(self, column=None, name=None, data=None, is_tree=False, value=None): if not licensed: raise LicenseException("use kx.Column objects") if name is not None: @@ -5879,21 +5889,32 @@ def __init__(self, column=None, name=None, value=None, is_tree=False): self._name = column self._renamed = False if value is not None: - self._value = value + self._data = value + self._renamed = True + warn("The kx.Column 'value' keyword is deprecated, please use 'data' instead", + DeprecationWarning) + elif data is not None: + self._data = data + self._renamed = True else: - self._value = column + self._data = column self._is_tree = is_tree + if self._name is None: + raise TypeError("Column 'name' value cannot be None.") + elif not isinstance(self._name, (str, SymbolAtom)): + raise TypeError("Column 'name' can only be of type Str and pykx.SymbolAtom") + def __repr__(self): preamble = f'pykx.{type(self).__name__}' - return f"{preamble}(name='{self._name}', value={type(self._value)})" + return f"{preamble}(name='{self._name}', data={type(self._data)})" """Function for building up a function call off a Column""" def call(self, op, *other, iterator=None, col_arg_ind=0, project_args=None): params = [] for param in other: if isinstance(param, Column): - param = param._value + param = param._data elif isinstance(param, Variable): param = param._name else: @@ -5904,7 +5925,7 @@ def call(self, op, *other, iterator=None, col_arg_ind=0, project_args=None): ): param = [param] params.append(param) - params.insert(col_arg_ind, self._value) + params.insert(col_arg_ind, self._data) if isinstance(op, (str, bytes, CharVector)): op = op.encode() if isinstance(op, str) else op cmd = [q(op)] @@ -5947,7 +5968,7 @@ def call(self, op, *other, iterator=None, col_arg_ind=0, project_args=None): i.extend(cmd) cmd = i cpy = copy.deepcopy(self) - cpy._value = cmd + cpy._data = cmd return cpy def __add__(self, other, iterator=None, col_arg_ind=0, project_args=None): @@ -6048,13 +6069,13 @@ def __and__(self, other): def __or__(self, other): if isinstance(other, Column): - other = other._value + other = other._data elif isinstance(other, QueryPhrase): raise TypeError("Cannot | off a Column with a QueryPhrase") elif isinstance(other, ParseTree): other = other._tree cpy = copy.deepcopy(self) - cpy._value = [q(b'or'), self._value, other] + cpy._data = [q(b'or'), self._data, other] return cpy def abs(self, iterator=None): @@ -7111,7 +7132,7 @@ def fby(int_or_class, by, aggregate, data, by_table=False, data_table=False): # name = by[0] elif isinstance(by, Column): name = by._name - by = by._value + by = by._data elif isinstance(by, QueryPhrase): name = by._names[0] by = by.to_dict() @@ -7120,7 +7141,7 @@ def fby(int_or_class, by, aggregate, data, by_table=False, data_table=False): # if isinstance(data, QueryPhrase): data = data.to_dict() pt = ParseTree.fby(by, aggregate, data, by_table, data_table) - return Column(name=name, value=pt) + return Column(name=name, data=pt) def fills(self, iterator=None): """ @@ -9612,6 +9633,39 @@ def xprev(self, other, iterator=None, col_arg_ind=1, project_args=None): return self.call('xprev', other, iterator=iterator, col_arg_ind=col_arg_ind, project_args=project_args) + @property + def time(self): + """ + Retrieve the time information from a temporal column + + + Examples: + + Retrieve time information from a column + + ```python + >>> import pykx as kx + >>> tab = kx.Table(data={'time': kx.random.random(3, kx.TimestampAtom.inf)}) + >>> tab + pykx.Table(pykx.q(' + time + ----------------------------- + 2118.11.10D05:05:45.270444032 + 2052.01.18D19:21:15.973214208 + 2088.03.13D18:56:07.388233728 + ')) + >>> tab.select(kx.Column('time').time) + pykx.Table(pykx.q(' + time + ------------ + 05:05:45.270 + 19:21:15.973 + 18:56:07.388 + ')) + ``` + """ + return self.call('`time$') + @property def hour(self): """ @@ -9886,11 +9940,44 @@ def name(self, name): ')) ``` """ + if not isinstance(name, (str, SymbolAtom)): + raise TypeError("'name' must be of type str or SymbolAtom") cpy = copy.deepcopy(self) cpy._name = name cpy._renamed = True return cpy + def data(self, data): + """ + Replace data of a column object + + Parameters: + data: The data to assign to the column + + Examples: + + Change the value of a column after selecting from table + ```python + >>> import pykx as kx + >>> tab = kx.Table(data={ + ... 'a': [1, -1, 0.5], + ... 'b': [[-1, 2, 1], [0, 1, 2], [1, 2, 3]] + ... }) + >>> tab.update(kx.Column('a').data([1,2,3])) + pykx.Table(pykx.q(' + a b + -------- + 1 -1 2 1 + 2 0 1 2 + 3 1 2 3 + ')) + ``` + """ + cpy = copy.deepcopy(self) + cpy._data = data + cpy._renamed=True + return cpy + def average(self, iterator=None): """ Calculate the average value for a column or items in a column @@ -10973,7 +11060,7 @@ def __init__(self, phrase, names=None, are_trees=False): elif isinstance(phrase, ParseTree): self._phrase = phrase._tree elif isinstance(phrase, Column): - self._phrase = [phrase._value] + self._phrase = [phrase._data] self._names = [phrase._name] self._are_trees = [phrase._is_tree] elif isinstance(phrase, str): @@ -10998,7 +11085,7 @@ def append(self, other): self._names.append('') self._are_trees.append(False) elif isinstance(other, Column): - self._phrase.append(other._value) + self._phrase.append(other._data) self._names.append(other._name) self._are_trees.append(other._is_tree) elif isinstance(other, QueryPhrase): @@ -11016,7 +11103,7 @@ def extend(self, other): self._names.extend('') self._are_trees.extend(False) elif isinstance(other, Column): - self._phrase.extend(other._value) + self._phrase.extend(other._data) self._names.extend(other._name) self._are_trees.extend(other._is_tree) elif isinstance(other, QueryPhrase): diff --git a/tests/test_ipc.py b/tests/test_ipc.py index acf7574..73c0916 100644 --- a/tests/test_ipc.py +++ b/tests/test_ipc.py @@ -945,6 +945,7 @@ async def test_debug_kwarg_async_global(q_port): q('.Q.sbt:.pykx_test.cache_sbt') +@pytest.mark.asyncio @pytest.mark.embedded async def test_debug_kwarg_embedded(kx, q): assert q('til 10', debug=True).py() == list(range(10)) diff --git a/tests/test_pykx.py b/tests/test_pykx.py index f8bde85..5eacebb 100644 --- a/tests/test_pykx.py +++ b/tests/test_pykx.py @@ -448,31 +448,31 @@ def test_kx_versions(kx): if test_vars == ('Linux', 'x86_64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Linux', 'x86_64', '4.1'): - assert kx.q.z.k == kx.q('2025.11.25') + assert kx.q.z.k == kx.q('2026.01.23') # elif test_vars == ('Linux', 'x86_64', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Linux', 'aarch64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Linux', 'aarch64', '4.1'): - assert kx.q.z.k == kx.q('2025.11.25') + assert kx.q.z.k == kx.q('2026.01.23') # elif test_vars == ('Linux', 'aarch64', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Darwin', 'x86_64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Darwin', 'x86_64', '4.1'): - assert kx.q.z.k == kx.q('2025.11.25') + assert kx.q.z.k == kx.q('2026.01.23') # elif test_vars == 'Darwin', 'x86_64', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Darwin', 'arm64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Darwin', 'arm64', '4.1'): - assert kx.q.z.k == kx.q('2025.11.25') + assert kx.q.z.k == kx.q('2026.01.23') # elif test_vars == ('Darwin', 'arm', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Windows', 'AMD64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Windows', 'AMD64', '4.1'): - assert kx.q.z.k == kx.q('2025.11.25') + assert kx.q.z.k == kx.q('2026.01.23') # elif test_vars == ('Windows', 'AMD64', '4.2'): # assert kx.q.z.k == kx.q('?') else: diff --git a/tests/test_query.py b/tests/test_query.py index 158b9fc..d36a992 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -592,6 +592,8 @@ def test_pythonic_query(kx): == table.select(columns=kx.Column('b').call('neg'))) assert all(kx.q('{select negb:neg b from x}', table) == table.select(columns=kx.Column('b', name='negb').call('neg'))) + assert all(kx.q('{select negb:neg b from x}', table) + == table.select(columns=kx.Column(name='negb', data=[kx.q('neg'), 'b']))) assert all(kx.q('{select negb:neg b from x}', table) == table.select(columns=kx.Column(name='negb', value=[kx.q('neg'), 'b']))) assert all(kx.q('{exec neg b from x}', table) @@ -756,6 +758,8 @@ def test_pythonic_query_ipc(kx, q_port): q.qsql.select('table', columns=kx.Column('b', name='negb').call('neg'))) assert q('{y~select negb:neg b from table}', q.qsql.select('table', columns=kx.Column(name='negb', value=[q('neg'), 'b']))) + assert q('{y~select negb:neg b from table}', + q.qsql.select('table', columns=kx.Column(name='negb', data=[q('neg'), 'b']))) assert q('{y~exec neg b from table}', q.qsql.exec('table', columns=kx.Column('b').call('neg'))) assert ({'asA': 'a', 'negB': [q('neg'), 'b']} == (kx.Column('a', name='asA')& kx.Column('b', name='negB').call('neg')).to_dict()) @@ -903,28 +907,28 @@ def test_column_licensed(kx): def test_fby_instance_call(kx): table = kx.q('([] x:`a`b`c;x1:1 2 3;x2:`a`e`g;x11:0 3 3;b:011b)') - assert kx.Column.fby(['c1,c2'], 'sum', table)._name == ['c1,c2'] + assert kx.Column.fby('c1', 'sum', table)._name == 'c1' with pytest.raises(RuntimeError) as err: - kx.Column('a').fby(['c1,c2'], 'sum', table) + kx.Column('a').fby('c1', 'sum', table) assert "Column object" in str(err) def test_no_inplace(kx): a = kx.Column('a') a.max() - assert a._value == 'a' + assert a._data == 'a' a.call('{x}') - assert a._value == 'a' + assert a._data == 'a' b = kx.Column('b') a | b - assert a._value == 'a' - assert b._value == 'b' + assert a._data == 'a' + assert b._data == 'b' a.name('aa') - assert a._value == 'a' + assert a._data == 'a' a & b - assert a._value == 'a' - assert b._value == 'b' + assert a._data == 'a' + assert b._data == 'b' pt = kx.ParseTree(['a']) pt.enlist() assert pt._tree == ['a'] diff --git a/tests/test_toq.py b/tests/test_toq.py index bd46197..57748ff 100644 --- a/tests/test_toq.py +++ b/tests/test_toq.py @@ -3,6 +3,7 @@ import math import os from pathlib import Path +from platform import system from sys import getrefcount from uuid import UUID, uuid4 @@ -1741,12 +1742,18 @@ def test_cast_setting(kx): def test_2d_array_from_file(kx): - df = pd.read_parquet('tests/nestedFloats.parquet') + basePath = Path(__file__).parent.parent.parent + file = 'nestedFloats.parquet' + testFilePath = (basePath/file) if system() == 'Windows' else 'tests/' + file + df = pd.read_parquet(testFilePath) kx.toq(df, no_allocator=True) def test_2d_array_from_file_no_allocator(kx): - df = pd.read_parquet('tests/nestedFloats.parquet') + basePath = Path(__file__).parent.parent.parent + file = 'nestedFloats.parquet' + testFilePath = (basePath/file) if system() == 'Windows' else 'tests/' + file + df = pd.read_parquet(testFilePath) kx.toq(df, no_allocator=True) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 5538246..3a4c2e4 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -14,6 +14,7 @@ from textwrap import dedent from uuid import UUID import itertools +import sys # Do not import Pandas, PyArrow, or PyKX here - use the pd/pa/kx fixtures instead! import numpy as np @@ -1677,7 +1678,10 @@ def test_index_iter(self, q): py_err = str(e) if kx_err is not None or py_err is not None: assert "is not in" in kx_err - assert "is not in" in py_err + if sys.version_info < (3, 14): + assert "is not in" in py_err + else: + assert "not in list" in py_err else: assert kx_res == py_res @@ -2092,11 +2096,8 @@ def test_ArrowUUIDType(q, kx, pa): kx.wrappers.ArrowUUIDType) assert isinstance(kx.wrappers.PandasUUIDArray(v), kx.wrappers.PandasUUIDArray) - with pytest.raises(ValueError, match=r'(?i)Cannot convert multiple chunks'): - pa.Table.from_arrays( - [pa.chunked_array([q('enlist 0Ng'), q('enlist 0Ng')])], - names=['a'] - ).to_pandas() + ca = pa.chunked_array([q('enlist 0Ng'), q('enlist 0Ng')]) + assert len(ca.chunks) == 2 def test_deserialize(kx): @@ -5725,3 +5726,49 @@ def test_to_vec(kx): def test_atom_ufunc(kx): assert 5 == np.add(kx.LongAtom(2), kx.LongAtom(3)).py() assert 2 == np.floor(kx.FloatAtom(2.5)).py() + + +class Test_Column: + def test_name_type(self, kx): + col = kx.Column('age', data=[25, 30, 31]) + assert col._name == 'age' + col = col.name('age_upd') + assert col._name == 'age_upd' + with pytest.raises(TypeError) as err: + col.name(123) + assert "str or SymbolAtom" in str(err) + with pytest.raises(TypeError) as err: + col = kx.Column(1, data=[25, 30, 31]) + assert "str or SymbolAtom" in str(err) + + with pytest.raises(TypeError) as err: + col = kx.Column(None, [25, 30, 31]) + assert "str or SymbolAtom" in str(err) + + col = kx.Column(kx.SymbolAtom('age'), data=[25, 30, 31]) + assert col._name == 'age' + + def test_column_positional(self, kx): + t = kx.q('([] a: 1 2 3)') + tc = kx.q('([] a: 1 2 3;c:1 2 3)') + assert kx.q('~', tc, t.update(kx.Column('a', 'c'))) + + def test_column_time(self, kx): + d1 = kx.TimestampAtom(2150, 10, 22, 20, 31, 15, 70713856) + d2 = kx.TimestampAtom(2025, 11, 25, 11, 9, 13, 20120214) + tab = kx.Table(data={'a': [d1, d2]}) + + assert all(tab.exec(kx.Column('a').time) == kx.TimeVector(kx.q('20:31:15.070 11:09:13.020'))) # noqa E:501 + + +def test_column_types(kx): + a = kx.Column(name='age', data=(1, 2, 3)) + assert (a._name=='age' and a._data==(1, 2, 3)) + + with pytest.raises(TypeError) as err: + kx.Column(data=(1, 2, 3)) + assert "'name' cannot be None" in str(err) + + with pytest.raises(TypeError) as err: + kx.Column(age=5, data=(1, 2, 3)) + assert "name can only be of type Str and pykx.SymbolAtom" in str(err)