diff --git a/.gitignore b/.gitignore index d172c3a..9b77cae 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ __pycache__/ !p.so !libq.* !q.* +!qdll.* !libe.so !e.* !libkurl.* diff --git a/MANIFEST.in b/MANIFEST.in index d28e4cf..d30fc86 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,18 +8,22 @@ graft src/pykx/lib graft src/pykx/extensions recursive-include src/pykx *.py *.pxd *.pyx *.c *.so *.k -recursive-include tests *.py include src/pykx/pykx.q include src/pykx/pykx_init.q_ -# When adding to this list a user should be conscious -# to additionally update the ignore pattern files in -# build/github.py +# Keep internal-only content out of the package. This list must be kept +# aligned with the ignore patterns in scripts/github.py, which keep the same +# content out of the public GitHub source repo - when adding to either list, +# update the other. Note the two use different matching: scripts/github.py +# matches by base name recursively at any depth, whereas MANIFEST.in matches +# anchored paths, so the entries below will not always look identical. exclude **/.gitignore exclude .gitlab-ci.yml exclude mkdocs.yml exclude **/pykx_init.q +exclude sonar-project.properties +global-exclude *.whl prune build prune coverage @@ -27,6 +31,12 @@ prune docs prune benchmarks prune examples prune tests +prune scripts +prune .gitlab +prune notebooks +prune docs_setup +prune docs_archive +prune _docker include docs/api/pykx-execution/q.md diff --git a/README.md b/README.md index d966548..c00d7a3 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,11 @@ Full instructions on license installation are covered on the [Installing page](d KX only officially supports versions of KDB-X Python built by KX, i.e. versions of KDB-X Python installed from wheel files. Support for user-built installations of KDB-X Python (e.g. built from the source distribution) is only provided on a best-effort basis. Currently, KDB-X Python provides wheels for the following environments: -- Linux (`manylinux2014_x86_64`, `manylinux2014_aarch64`) with CPython 3.9-3.14 -- macOS (`macosx_10_15_x86_64`, `macosx_10_15_arm64`) with CPython 3.9-3.14 -- Windows (`win_amd64`) with CPython 3.9-3.14 +- **Linux** + - (`manylinux2014_x86_64`, `manylinux2014_aarch64`) with CPython 3.9-3.11 + - (`manylinux_2_28_x86_64`, `manylinux_2_28_aarch64`) with CPython 3.12-3.14 +- **macOS** (`macosx_10_15_x86_64`, `macosx_10_15_arm64`) with CPython 3.9-3.14 +- **Windows** (`win_amd64`) with CPython 3.9-3.14 ### Dependencies @@ -185,7 +187,7 @@ This work is dual licensed under [Apache 2.0](https://code.kx.com/pykx/license.h ## Community Help -If you have any issues or questions you can post them to [community.kx.com](https://community.kx.com/). Also available on Stack Overflow are the tags [pykx](https://stackoverflow.com/questions/tagged/pykx) and [kdb](https://stackoverflow.com/questions/tagged/kdb). +If you have any issues or questions you can post them to the [KX Forum](https://forum.kx.com/). Also available on Stack Overflow are the tags [pykx](https://stackoverflow.com/questions/tagged/pykx) and [kdb](https://stackoverflow.com/questions/tagged/kdb). ## Customer Support diff --git a/custom_theme/main.html b/custom_theme/main.html index b56eff9..bffbb07 100644 --- a/custom_theme/main.html +++ b/custom_theme/main.html @@ -8,15 +8,4 @@ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-PG938LS'); - - - {% endblock %} diff --git a/custom_theme/partials/header.html.tpl b/custom_theme/partials/header.html.tpl index 3e9f6a0..f0315ae 100644 --- a/custom_theme/partials/header.html.tpl +++ b/custom_theme/partials/header.html.tpl @@ -25,6 +25,11 @@ var currVersion = '{{ config.curr_docs_version }}'
-
@@ -85,11 +90,14 @@ {% endif %} - {% if "search" in config["plugins"] %} - - {% include "partials/search.html" %} + {% if "material/search" in config.plugins %} + {% set search = config.plugins["material/search"] | attr("config") %} + {% if search.enabled %} + + {% include "partials/search.html" %} + {% endif %} {% endif %} {% if config.repo_url %}
@@ -103,28 +111,3 @@ {% endif %} {% endif %} - - - - diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 0000000..87f5880 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,83 @@ +--- +title: Page not found +description: The requested KDB-X Python documentation page could not be found. +hide: + - navigation + - toc +search: + exclude: true +--- + +
+
+ +
+

404: Page not found

+

+ Don't worry – even the best Python code sometimes raises an exception. + This page may have moved, been renamed, or become unavailable. +

+ +
+
+ +
+
+

Find your way

+

+ Use the site search above, or choose a KDB-X Python documentation area. +

+ +
+ +
+
+

Documentation feedback

+ + Report a broken link + +
+
+

Community

+ +
+
+

Customer support

+ KX Support Portal +
+
+
+
diff --git a/docs/api/pykx-execution/q.md b/docs/api/pykx-execution/q.md index 6e9995d..3323700 100644 --- a/docs/api/pykx-execution/q.md +++ b/docs/api/pykx-execution/q.md @@ -48,7 +48,7 @@ Get the value of an environment variable. ```python >>> pykx.q.getenv('EDITOR') -pykx.CharVector(q('"nvim"')) +pykx.CharVector(pykx.q('"nvim"')) ``` ### [gtime](https://code.kx.com/q/ref/gtime/) @@ -58,7 +58,7 @@ UTC equivalent of local timestamp. ```python >>> import datetime >>> pykx.q.gtime(datetime.datetime.fromisoformat('2022-05-22T12:23:45.123')) -pykx.TimestampAtom(q('2022.05.22D16:23:45.123000000')) +pykx.TimestampAtom(pykx.q('2022.05.22D16:23:45.123000000')) ``` ### [ltime](https://code.kx.com/q/ref/gtime/#ltime) @@ -68,7 +68,7 @@ Local equivalent of UTC timestamp. ```python >>> import datetime >>> pykx.q.ltime(datetime.datetime.fromisoformat('2022-05-22T12:23:45.123')) -pykx.TimestampAtom(q('2022.05.22D08:23:45.123000000')) +pykx.TimestampAtom(pykx.q('2022.05.22D08:23:45.123000000')) ``` @@ -79,7 +79,7 @@ Set the value of an environment variable. ```python >>> pykx.q.setenv('RTMP', b'/home/user/temp') >>> pykx.q.getenv('RTMP') -pykx.CharVector(q('"/home/user/temp"')) +pykx.CharVector(pykx.q('"/home/user/temp"')) ``` ## Interpret @@ -90,7 +90,7 @@ Evaluate parse trees. ```python >>> pykx.q.eval([pykx.q('+'), 2, 3]) -pykx.LongAtom(q('5')) +pykx.LongAtom(pykx.q('5')) ``` ### [parse](https://code.kx.com/q/ref/parse/) @@ -99,7 +99,7 @@ Parse a char vector into a parse tree, which can be evaluated with [`pykx.q.eval ```python >>> pykx.q.parse(b'{x * x}') -pykx.Lambda(q('{x * x}')) +pykx.Lambda(pykx.q('{x * x}')) >>> pykx.q.parse(b'2 + 3') pykx.List(pykx.q(' + @@ -116,7 +116,7 @@ Behaves similar to [`eval`](#eval) except the evaluation is blocked from modifyi ```python >>> pykx.q.reval(pykx.q.parse(b'til 10')) -pykx.LongVector(q('0 1 2 3 4 5 6 7 8 9')) +pykx.LongVector(pykx.q('0 1 2 3 4 5 6 7 8 9')) ``` ### [show](https://code.kx.com/q/ref/show/) @@ -133,7 +133,7 @@ Note: `show` bypasses typical Python output redirection. 2 3 4 -pykx.Identity(q('::')) +pykx.Identity(pykx.q('::')) ``` ### [system](https://code.kx.com/q/ref/system/) @@ -144,7 +144,7 @@ Where x is a string representing a [system command](https://code.kx.com/q/basics ```python >>> pykx.q.system(b'pwd') -pykx.List(q('"/home/user"')) +pykx.List(pykx.q('"/home/user"')) ``` ### [value](https://code.kx.com/q/ref/value/) @@ -168,7 +168,7 @@ Returns the value of x. ```python >>> pykx.q.value(pykx.q('`q`w`e!(1 2; 3 4; 5 6)')) -pykx.List(q(' +pykx.List(pykx.q(' 1 2 3 4 5 6 @@ -185,7 +185,7 @@ Write global tables to disk as splayed, enumerated, indexed q tables. >>> from pathlib import Path >>> pykx.q['t'] = kx.Table(data={'x': [1, 2, 3], 'y': [10, 20, 30]}) >>> pykx.q.dsave(Path('v'), 't') -pykx.SymbolAtom(q('`t')) +pykx.SymbolAtom(pykx.q('`t')) ``` ### [get](https://code.kx.com/q/ref/get/) @@ -195,28 +195,31 @@ Read or memory-map a variable or q data file. ```python >>> pykx.q['a'] = 10 >>> pykx.q.get('a') -pykx.LongAtom(q('10')) +pykx.LongAtom(pykx.q('10')) ``` ### [hclose](https://code.kx.com/q/ref/hopen/#hclose) Where x is a connection handle, closes the connection, and destroys the handle. ```python ->>> pykx.q.hclose(pykx.q('3i')) +>>> pykx.q.hclose(3) +pykx.Identity(pykx.q('::')) ``` ### [hcount](https://code.kx.com/q/ref/hcount/) Size of a file in bytes. ```python >>> pykx.q.hcount('example.txt') -pykx.LongAtom(q('11')) +pykx.LongAtom(pykx.q('11')) ``` ### [hdel](https://code.kx.com/q/ref/hdel/) Where `x` is a [file symbol atom](#hsym), deletes the file or folder (if empty), and returns `x`. ```python ->>> pykx.q.hdel('example.txt') +>>> from pathlib import Path +>>> pykx.q.hdel(Path('example.txt')) +pykx.SymbolAtom(pykx.q('`:example.txt')) ``` ### [hopen](https://code.kx.com/q/ref/hopen/) @@ -224,8 +227,9 @@ Where `x` is a [file symbol atom](#hsym), deletes the file or folder (if empty), Open a connection to a file or process. ```python ->>> pykx.q.hopen('example.txt') -pykx.IntAtom(q('3i')) +>>> from pathlib import Path +>>> pykx.q.hopen(Path('example.txt')) +pykx.IntAtom(pykx.q('3i')) ``` ### [hsym](https://code.kx.com/q/ref/hsym/) @@ -234,7 +238,7 @@ Convert symbols to handle symbols, which can be used for I/O as file descriptors ```python >>> pykx.q.hsym('10.43.23.197') -pykx.SymbolAtom(q('`:10.43.23.197')) +pykx.SymbolAtom(pykx.q('`:10.43.23.197')) ``` ### [load](https://code.kx.com/q/ref/load/) @@ -276,7 +280,7 @@ Read text from a file or process handle. ```python >>> pykx.q.read0('example.txt') -pykx.List(q(' +pykx.List(pykx.q(' "Hello" "World" ')) @@ -288,7 +292,7 @@ Read bytes from a file or named pipe. ```python >>> pykx.q.read1('example.txt') -pykx.ByteVector(q('0x48656c6c6f0a576f726c64')) +pykx.ByteVector(pykx.q('0x48656c6c6f0a576f726c64')) ``` ### [rload](https://code.kx.com/q/ref/load/#rload) @@ -298,7 +302,7 @@ Load a splayed table from a directory. ```python >>> pykx.q.rload('t') >>> pykx.q('t') -pykx.Table(q(' +pykx.Table(pykx.q(' x y ---- 1 10 @@ -314,7 +318,7 @@ Write a table splayed to a directory. ```python >>> pykx.q['t'] = pykx.Table([[1, 10], [2, 20], [3, 30]]) >>> pykx.q.rsave('t') -pykx.SymbolAtom(q('`:t/')) +pykx.SymbolAtom(pykx.q('`:t/')) ``` ### [save](https://code.kx.com/q/ref/save/) @@ -324,7 +328,7 @@ Write global data to file or splayed to a directory. ```python >>> pykx.q['t'] = pykx.Table([[1, 10], [2, 20], [3, 30]]) >>> pykx.q.save('t') -pykx.SymbolAtom(q('`:t')) +pykx.SymbolAtom(pykx.q('`:t')) ``` ### [set](https://code.kx.com/q/ref/get/#set) @@ -362,9 +366,9 @@ Where ```python >>> pykx.q.set('a', 42) -pykx.SymbolAtom(q('`a')) +pykx.SymbolAtom(pykx.q('`a')) >>> pykx.q('a') -pykx.LongAtom(q('42')) +pykx.LongAtom(pykx.q('42')) ``` ## Iterate @@ -375,11 +379,11 @@ Iterate over list and apply a function to each element. ```python >>> pykx.q.each(pykx.q.count, [b'Tis', b'but', b'a', b'scratch']) -pykx.LongVector(q('3 3 1 7')) +pykx.LongVector(pykx.q('3 3 1 7')) >>> pykx.q.each(pykx.q.sums, [[2, 3, 4], [[5, 6], [7, 8]], [9, 10, 11, 12]]) -pykx.List(q(' +pykx.List(pykx.q(' 2 5 9 -((5;6);12 14) +(5 6;12 14) 9 19 30 42 ')) ``` @@ -392,7 +396,7 @@ Just as with Over and Scan, over and scan share the same syntax and perform the ```python >>> pykx.q.over(pykx.q('*'), [1, 2, 3, 4, 5]) -pykx.LongAtom(q('120')) +pykx.LongAtom(pykx.q('120')) ``` ### [peach](https://code.kx.com/q/ref/each/) @@ -401,11 +405,11 @@ pykx.LongAtom(q('120')) ```python >>> pykx.q.peach(pykx.q.count, [b'Tis', b'but', b'a', b'scratch']) -pykx.LongVector(q('3 3 1 7')) +pykx.LongVector(pykx.q('3 3 1 7')) >>> pykx.q.peach(pykx.q.sums, [[2, 3, 4], [[5, 6], [7, 8]], [9, 10, 11, 12]]) -pykx.List(q(' +pykx.List(pykx.q(' 2 5 9 -((5;6);12 14) +(5 6;12 14) 9 19 30 42 ')) ``` @@ -417,8 +421,8 @@ Applies a function to each item of `x` and the item preceding it, and returns a ```python >>> pykx.q.prior(pykx.q('+'), [1, 2, 3, 4, 5]) pykx.LongVector(pykx.q('1 3 5 7 9')) ->>> pykx.q.prior(lambda x, y: x + y, pykx.LongVector([1, 2, 3, 4, 5])) -pykx.LongVector(pykx.q('0N 3 5 7 9')) +>>> pykx.q.prior(pykx.q('+'), pykx.LongVector([1, 2, 3, 4, 5])) +pykx.LongVector(pykx.q('1 3 5 7 9')) ``` ### [scan](https://code.kx.com/q/ref/over/) @@ -429,7 +433,7 @@ Just as with Over and Scan, over and scan share the same syntax and perform the ```python >>> pykx.q.scan(pykx.q('+'), [1, 2, 3, 4, 5]) -pykx.LongVector(q('1 3 6 10 15')) +pykx.LongVector(pykx.q('1 3 6 10 15')) ``` ## Join @@ -452,12 +456,12 @@ The resulting time column is the value of the boundary used in the first table. ... 'sym': ['ibm', 'msft', 'msft', 'ibm'], 'qty': [100, 99, 101, 98] ... }) >>> pykx.q.aj(pykx.SymbolVector(['sym', 'time']), df1, df2) -pykx.Table(q(' -time sym qty ------------------------------ -0D10:01:01.000000000 msft 101 -0D10:01:03.000000000 ibm 98 -0D10:01:04.000000000 ge 150 +pykx.Table(pykx.q(' +time sym qty +----------------- +10:01:01 msft 101 +10:01:03 ibm 98 +10:01:04 ge 150 ')) ``` @@ -479,12 +483,12 @@ The resulting time column is the actual time of the last value in the second tab ... 'sym': ['ibm', 'msft', 'msft', 'ibm'], 'qty': [100, 99, 101, 98] ... }) >>> pykx.q.aj0(pykx.SymbolVector(['sym', 'time']), df1, df2) -pykx.Table(q(' -time sym qty ------------------------------ -0D10:01:00.000000000 msft 101 -0D10:01:02.000000000 ibm 98 -0D10:01:04.000000000 ge 150 +pykx.Table(pykx.q(' +time sym qty +----------------- +10:01:00 msft 101 +10:01:02 ibm 98 +10:01:04 ge 150 ')) ``` @@ -509,11 +513,11 @@ The resulting time column is the value of the boundary used in the first table. ... 'p': pykx.q('1 0N') ... }) >>> pykx.q.ajf(pykx.SymbolVector(['sym', 'time']), df1, df2) -pykx.Table(q(' -time sym p n ----------------------------- -0D00:00:01.000000000 a 1 r -0D00:00:01.000000000 b 1 s +pykx.Table(pykx.q(' +time sym p n +---------------- +00:00:01 a 1 r +00:00:01 b 1 s ')) ``` @@ -538,11 +542,11 @@ The resulting time column is the actual time of the last value in the second tab ... 'p': pykx.q('1 0N') ... }) >>> pykx.q.ajf0(pykx.SymbolVector(['sym', 'time']), df1, df2) -pykx.Table(q(' -time sym p n ----------------------------- -0D00:00:01.000000000 a 1 r -0D00:00:01.000000000 b 1 s +pykx.Table(pykx.q(' +time sym p n +---------------- +00:00:01 a 1 r +00:00:01 b 1 s ')) ``` @@ -558,7 +562,7 @@ Performs an as-of join across temporal columns in tables. The last column the se ... 'sym': ['a', 'a', 'b', 'b'], 'p': pykx.LongVector([2, 4, 6, 8])}) >>> df2 = pd.DataFrame({'sym':['b'], 'time': np.array([3], dtype='timedelta64[s]')}) >>> pykx.q.asof(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' p - 6 @@ -574,7 +578,7 @@ Equi join. The result has one combined record for each row in the second table t >>> df1 = pd.DataFrame({'sym':['a', 'a', 'b', 'a', 'c', 'b', 'c', 'a'], 'p': pykx.LongVector([2, 4, 6, 8, 1, 3, 5, 7])}) >>> df2 = pd.DataFrame({'sym':['a', 'b'], 'w': ['alpha', 'beta']}) >>> pykx.q.ej('sym', df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p w ----------- a 2 alpha @@ -596,7 +600,7 @@ Inner join. The result has one combined record for each row in the first table t >>> df2 = pd.DataFrame({'sym':['IBM', 'MSFT'], 'ex': ['N', 'CME'], 'MC': pykx.LongVector([1000, 250])}) >>> df2 = pykx.q.xkey('sym', df2) >>> pykx.Table(df1) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ IBM 7 @@ -607,14 +611,14 @@ IBM 2 MSFT 5 ')) >>> df2 -pykx.KeyedTable(q(' +pykx.KeyedTable(pykx.q(' sym | ex MC ----| -------- IBM | N 1000 MSFT| CME 250 ')) >>> pykx.q.ij(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ex MC --------------- IBM 7 N 1000 @@ -631,9 +635,9 @@ Inner join nulls filled. The result has one combined record for each row in the >>> import pandas as pd >>> df1 = pd.DataFrame({'sym':['IBM', 'FDP', 'FDP', 'FDP', 'IBM', 'MSFT'], 'p': pykx.LongVector([7, 8, 6, 5, 2, 5])}) >>> df2 = pd.DataFrame({'sym':['IBM', 'MSFT'], 'ex': ['N', 'CME'], 'MC': pykx.LongVector([1000, 250])}) ->>> b = pykx.q.xkey('sym', df2) +>>> df2 = pykx.q.xkey('sym', df2) >>> pykx.Table(df1) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ IBM 7 @@ -644,14 +648,14 @@ IBM 2 MSFT 5 ')) >>> df2 -pykx.KeyedTable(q(' +pykx.KeyedTable(pykx.q(' sym | ex MC ----| -------- IBM | N 1000 MSFT| CME 250 ')) >>> pykx.q.ijf(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ex MC --------------- IBM 7 N 1000 @@ -668,9 +672,9 @@ Left join. For each record in the first table, the result has one record with th >>> import pandas as pd >>> df1 = pd.DataFrame({'sym':['IBM', 'FDP', 'FDP', 'FDP', 'IBM', 'MSFT'], 'p': pykx.LongVector([7, 8, 6, 5, 2, 5])}) >>> df2 = pd.DataFrame({'sym':['IBM', 'MSFT'], 'ex': ['N', 'CME'], 'MC': pykx.LongVector([1000, 250])}) ->>> b = pykx.q.xkey('sym', df2) ->>> pykx.Table(df2) -pykx.Table(q(' +>>> df2 = pykx.q.xkey('sym', df2) +>>> pykx.Table(df1) +pykx.Table(pykx.q(' sym p ------ IBM 7 @@ -680,15 +684,15 @@ FDP 5 IBM 2 MSFT 5 ')) ->>> df1 -pykx.KeyedTable(q(' +>>> df2 +pykx.KeyedTable(pykx.q(' sym | ex MC ----| -------- IBM | N 1000 MSFT| CME 250 ')) >>> pykx.q.lj(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ex MC --------------- IBM 7 N 1000 @@ -708,9 +712,9 @@ Left join nulls filled. For each record in the first table, the result has one r >>> import pandas as pd >>> df1 = pd.DataFrame({'sym':['IBM', 'FDP', 'FDP', 'FDP', 'IBM', 'MSFT'], 'p': pykx.LongVector([7, 8, 6, 5, 2, 5])}) >>> df2 = pd.DataFrame({'sym':['IBM', 'MSFT'], 'ex': ['N', 'CME'], 'MC': pykx.LongVector([1000, 250])}) ->>> b = pykx.q.xkey('sym', df2) +>>> df2 = pykx.q.xkey('sym', df2) >>> pykx.Table(df1) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ IBM 7 @@ -720,15 +724,15 @@ FDP 5 IBM 2 MSFT 5 ')) ->>> df1 -pykx.KeyedTable(q(' +>>> df2 +pykx.KeyedTable(pykx.q(' sym | ex MC ----| -------- IBM | N 1000 MSFT| CME 250 ')) >>> pykx.q.ljf(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ex MC --------------- IBM 7 N 1000 @@ -748,7 +752,7 @@ Plus join. For each record in the first table, the result has one record with th >>> import pandas as pd >>> df1 = pd.DataFrame({'a': pykx.LongVector([1, 2, 3]), 'b':['x', 'y', 'z'], 'c': pykx.LongVector([10, 20, 30])}) >>> pykx.Table(df1) -pykx.Table(q(' +pykx.Table(pykx.q(' a b c ------ 1 x 10 @@ -762,14 +766,14 @@ a b c ... 'd': pykx.LongVector([10, 20]) ... }) >>> df2 = pykx.q.xkey(pykx.SymbolVector(['a', 'b']), df2) -pykx.KeyedTable(q(' +pykx.KeyedTable(pykx.q(' a b| c d ---| ---- 1 x| 1 10 3 z| 2 20 ')) >>> pykx.q.pj(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' a b c d --------- 1 x 11 10 @@ -799,7 +803,7 @@ Union join. Where the first table and the second table are both keyed or both un 0 IBM N 1000 1 MSFT CME 250 >>> pykx.q.uj(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ex MC --------------- IBM 7 @@ -834,7 +838,7 @@ Union join nulls filled. Where the first table and the second table are both key 0 IBM N 1000 1 MSFT CME 250 >>> pykx.q.ujf(df1, df2) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ex MC --------------- IBM 7 @@ -855,7 +859,7 @@ Window join. Returns for each record in the table, a record with additional colu ```python >>> import pandas as pd >>> import numpy as np ->>> pykx.q('t: ([]sym:3#`ibm;time:10:01:01 10:01:04 10:01:08;price:100 101 105)') +>>> pykx.q('t: ([]sym:3#`ibm;time:10:01:01 10:01:04 10:01:08;price:100 101 105); t') pykx.Table(pykx.q(' sym time price ------------------ @@ -873,6 +877,8 @@ ibm 10:01:08 105 1 ibm 0 days 10:01:04 101 2 ibm 0 days 10:01:08 105 >>> pykx.q('q:([]sym:`ibm; time:10:01:01+til 9; ask: (101 103 103 104 104 107 108 107 108); bid: (98 99 102 103 103 104 106 106 107))') +pykx.Identity(pykx.q('::')) +>>> pykx.q['q'] pykx.Table(pykx.q(' sym time ask bid -------------------- @@ -905,7 +911,7 @@ Window join. Returns for each record in the table, a record with additional colu ```python >>> import pandas as pd >>> import numpy as np ->>> pykx.q('t: ([]sym:3#`ibm;time:10:01:01 10:01:04 10:01:08;price:100 101 105)') +>>> pykx.q('t: ([]sym:3#`ibm;time:10:01:01 10:01:04 10:01:08;price:100 101 105); t') pykx.Table(pykx.q(' sym time price ------------------ @@ -923,6 +929,8 @@ ibm 10:01:08 105 1 ibm 0 days 10:01:04 101 2 ibm 0 days 10:01:08 105 >>> pykx.q('q:([]sym:`ibm; time:10:01:01+til 9; ask: (101 103 103 104 104 107 108 107 108); bid: (98 99 102 103 103 104 106 106 107))') +pykx.Identity(pykx.q('::')) +>>> pykx.q['q'] pykx.Table(pykx.q(' sym time ask bid -------------------- @@ -956,7 +964,7 @@ Count the items of a list or dictionary. ```python >>> pykx.q.count([1, 2, 3]) -pykx.LongAtom(q('3')) +pykx.LongAtom(pykx.q('3')) ``` ### [cross](https://code.kx.com/q/ref/cross/) @@ -965,7 +973,7 @@ Returns all possible combinations of x and y. ```python >>> pykx.q.cross([1, 2, 3], [4, 5, 6]) -pykx.List(q(' +pykx.List(pykx.q(' 1 4 1 5 1 6 @@ -984,7 +992,7 @@ Cut a list or table into sub-arrays. ```python >>> pykx.q.cut(3, range(10)) -pykx.List(q(' +pykx.List(pykx.q(' 0 1 2 3 4 5 6 7 8 @@ -998,7 +1006,7 @@ Returns a list with its arguments as items. ```python >>> pykx.q.enlist(1, 2, 3, 4) -pykx.LongVector(q('1 2 3 4')) +pykx.LongVector(pykx.q('1 2 3 4')) ``` ### [fills](https://code.kx.com/q/ref/fills/) @@ -1008,7 +1016,7 @@ Replace nulls with preceding non-nulls. ```python >>> a = pykx.q('0N 1 2 0N 0N 2 3 4 5 0N 4') >>> pykx.q.fills(a) -pykx.LongVector(q('0N 1 2 2 2 2 3 4 5 5 4')) +pykx.LongVector(pykx.q('0N 1 2 2 2 2 3 4 5 5 4')) ``` ### [first](https://code.kx.com/q/ref/first/) @@ -1016,7 +1024,7 @@ pykx.LongVector(q('0N 1 2 2 2 2 3 4 5 5 4')) First item of a list ```python >>> pykx.q.first([1, 2, 3, 4, 5]) -pykx.LongAtom(q('1')) +pykx.LongAtom(pykx.q('1')) ``` ### [flip](https://code.kx.com/q/ref/flip/) @@ -1025,7 +1033,7 @@ Returns x transposed, where x may be a list of lists, a dictionary or a table. ```python >>> pykx.q.flip([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]) -pykx.List(q(' +pykx.List(pykx.q(' 1 6 2 7 3 8 @@ -1042,7 +1050,7 @@ The order of the keys is the order in which they appear in x. ```python >>> pykx.q.group(b'mississippi') -pykx.Dictionary(q(' +pykx.Dictionary(pykx.q(' m| ,0 i| 1 4 7 10 s| 2 3 5 6 @@ -1056,7 +1064,7 @@ Intersection of two lists or dictionaries. ```python >>> pykx.q.inter([1, 2, 3], [2, 3, 4]) -pykx.LongVector(q('2 3')) +pykx.LongVector(pykx.q('2 3')) ``` ### [last](https://code.kx.com/q/ref/first/#last) @@ -1065,7 +1073,7 @@ Last item of a list ```python >>> pykx.q.last([1, 2, 3]) -pykx.LongAtom(q('3')) +pykx.LongAtom(pykx.q('3')) ``` ### [mcount](https://code.kx.com/q/ref/count/#mcount) @@ -1074,7 +1082,7 @@ Returns the x-item moving counts of the non-null items of y. The first x items o ```python >>> pykx.q.mcount(3, pykx.q('1 2 3 4 5 0N 6 7 8')) -pykx.IntVector(q('1 2 3 3 3 2 2 2 3i')) +pykx.IntVector(pykx.q('1 2 3 3 3 2 2 2 3i')) ``` ### [next](https://code.kx.com/q/ref/next/) @@ -1083,7 +1091,7 @@ Next items in a list. ```python >>> pykx.q.next([1, 2, 3, 4]) -pykx.LongVector(q('2 3 4 0N')) +pykx.LongVector(pykx.q('2 3 4 0N')) ``` ### [prev](https://code.kx.com/q/ref/next/#prev) @@ -1092,7 +1100,7 @@ Immediately preceding items in a list. ```python >>> pykx.q.prev([1, 2, 3, 4]) -pykx.LongVector(q('0N 1 2 3')) +pykx.LongVector(pykx.q('0N 1 2 3')) ``` ### [raze](https://code.kx.com/q/ref/raze/) @@ -1101,7 +1109,7 @@ Return the items of x joined, collapsing one level of nesting. ```python >>> pykx.q.raze([[1, 2], [3, 4]]) -pykx.LongVector(q('1 2 3 4')) +pykx.LongVector(pykx.q('1 2 3 4')) ``` ### [reverse](https://code.kx.com/q/ref/reverse/) @@ -1110,13 +1118,7 @@ Reverse the order of items of a list or dictionary. ```python >>> pykx.q.reverse([1, 2, 3, 4, 5]) -pykx.List(q(' -5 -4 -3 -2 -1 -')) +pykx.LongVector(pykx.q('5 4 3 2 1')) ``` ### [rotate](https://code.kx.com/q/ref/rotate/) @@ -1125,7 +1127,7 @@ Shift the items of a list to the left or right. ```python >>> pykx.q.rotate(2, [1, 2, 3, 4, 5]) -pykx.LongVector(q('3 4 5 1 2')) +pykx.LongVector(pykx.q('3 4 5 1 2')) ``` ### [sublist](https://code.kx.com/q/ref/sublist/) @@ -1134,7 +1136,7 @@ Select a sublist of a list. ```python >>> pykx.q.sublist(2, [1, 2, 3, 4, 5]) -pykx.LongVector(q('1 2')) +pykx.LongVector(pykx.q('1 2')) ``` ### [sv](https://code.kx.com/q/ref/sv/) @@ -1146,7 +1148,7 @@ pykx.LongVector(q('1 2')) ```python >>> pykx.q.sv(10, [1, 2, 3, 4]) -pykx.LongAtom(q('1234')) +pykx.LongAtom(pykx.q('1234')) ``` ### [til](https://code.kx.com/q/ref/til/) @@ -1155,7 +1157,7 @@ First x natural numbers. ```python >>> pykx.q.til(10) -pykx.LongVector(q('0 1 2 3 4 5 6 7 8 9')) +pykx.LongVector(pykx.q('0 1 2 3 4 5 6 7 8 9')) ``` ### [union](https://code.kx.com/q/ref/union/) @@ -1164,7 +1166,7 @@ Union of two lists. ```python >>> pykx.q.union([1, 2, 3, 3, 5], [2, 4, 6, 8]) -pykx.LongVector(q('1 2 3 5 4 6 8')) +pykx.LongVector(pykx.q('1 2 3 5 4 6 8')) ``` ### [vs](https://code.kx.com/q/ref/vs/) @@ -1176,7 +1178,7 @@ pykx.LongVector(q('1 2 3 5 4 6 8')) ```python >>> pykx.q.vs(b',', b'one,two,three') -pykx.List(q(' +pykx.List(pykx.q(' "one" "two" "three" @@ -1189,9 +1191,9 @@ Copies of indexes of a list or keys of a dictionary. ```python >>> pykx.q.where(pykx.BooleanVector([True, False, True, True, False])) -pykx.LongVector(q('0 2 3')) +pykx.LongVector(pykx.q('0 2 3')) >>> pykx.q.where(pykx.q('1 0 0 1 0 1 1')) -pykx.LongVector(q('0 3 5 6')) +pykx.LongVector(pykx.q('0 3 5 6')) ``` ### [xprev](https://code.kx.com/q/ref/next/#xprev) @@ -1200,14 +1202,14 @@ Nearby items in a list. ```python >>> pykx.q.xprev(2, [1, 2, 3, 4, 5, 6]) -pykx.LongVector(q('0N 0N 1 2 3 4')) +pykx.LongVector(pykx.q('0N 0N 1 2 3 4')) ``` There is no `xnext` function, but `xprev` with a negative number as its first argument can achieve this. ```python >>> pykx.q.xprev(-2, [1, 2, 3, 4, 5, 6]) -pykx.LongVector(q('3 4 5 6 0N 0N')) +pykx.LongVector(pykx.q('3 4 5 6 0N 0N')) ``` ## Logic @@ -1218,9 +1220,9 @@ Everything is true. ```python >>> pykx.q.all([True, True, True, True]) -pykx.BooleanAtom(q('1b')) +pykx.BooleanAtom(pykx.q('1b')) >>> pykx.q.all([True, True, False, True]) -pykx.BooleanAtom(q('0b')) +pykx.BooleanAtom(pykx.q('0b')) ``` ### [any](https://code.kx.com/q/ref/all-any/#any) @@ -1229,9 +1231,9 @@ Something is true. ```python >>> pykx.q.any([False, False, True, False]) -pykx.BooleanAtom(q('1b')) +pykx.BooleanAtom(pykx.q('1b')) >>> pykx.q.any([False, False]) -pykx.BooleanAtom(q('0b')) +pykx.BooleanAtom(pykx.q('0b')) ``` ## Math @@ -1242,7 +1244,7 @@ Where x is a numeric or temporal, returns the absolute value of x. Null is retur ```python >>> pykx.q.abs(-5) -pykx.LongAtom(q('5')) +pykx.LongAtom(pykx.q('5')) ``` ### [acos](https://code.kx.com/q/ref/cos/) @@ -1251,7 +1253,7 @@ The arccosine of x; that is, the value whose cosine is x. The result is in radia ```python >>> pykx.q.acos(0.5) -pykx.FloatAtom(q('1.047198')) +pykx.FloatAtom(pykx.q('1.047198')) ``` ### [asin](https://code.kx.com/q/ref/sin/) @@ -1260,7 +1262,7 @@ The arcsine of x; that is, the value whose sine is x. The result is in radians a ```python >>> pykx.q.asin(0.5) -pykx.FloatAtom(q('0.5235988')) +pykx.FloatAtom(pykx.q('0.5235988')) ``` ### [atan](https://code.kx.com/q/ref/tan/) @@ -1268,7 +1270,7 @@ pykx.FloatAtom(q('0.5235988')) The arctangent of x; that is, the value whose tangent is x. The result is in radians and lies between -π / 2 and π / 2. ```python >>> pykx.q.atan(0.5) -pykx.FloatAtom(q('0.4636476')) +pykx.FloatAtom(pykx.q('0.4636476')) ``` ### [avg](https://code.kx.com/q/ref/avg/#avg) @@ -1277,7 +1279,7 @@ Arithmetic mean. ```python >>> pykx.q.avg([1, 2, 3, 4, 7]) -pykx.FloatAtom(q('3.4')) +pykx.FloatAtom(pykx.q('3.4')) ``` ### [avgs](https://code.kx.com/q/ref/avg/#avgs) @@ -1286,7 +1288,7 @@ Running mean. ```python >>> pykx.q.avgs([1, 2, 3, 4, 7]) -pykx.FloatVector(q('1 1.5 2 2.5 3.4')) +pykx.FloatVector(pykx.q('1 1.5 2 2.5 3.4')) ``` ### [ceiling](https://code.kx.com/q/ref/ceiling/) @@ -1295,7 +1297,7 @@ Round up. ```python >>> pykx.q.ceiling([-2.7, -1.1, 0, 1.1, 2.7]) -pykx.LongVector(q('-2 -1 0 2 3')) +pykx.LongVector(pykx.q('-2 -1 0 2 3')) ``` ### [cor](https://code.kx.com/q/ref/cor/) @@ -1304,7 +1306,7 @@ Correlation. ```python >>> pykx.q.cor(pykx.LongVector([29, 10, 54]), pykx.LongVector([1, 3, 9])) -pykx.FloatAtom(q('0.7727746')) +pykx.FloatAtom(pykx.q('0.7727746')) ``` ### [cos](https://code.kx.com/q/ref/cos/) @@ -1313,7 +1315,7 @@ The cosine of x, taken to be in radians. The result is between -1 and 1, or null ```python >>> pykx.q.cos(0.2) -pykx.FloatAtom(q('0.9800666')) +pykx.FloatAtom(pykx.q('0.9800666')) ``` ### [cov](https://code.kx.com/q/ref/cov/) @@ -1322,7 +1324,7 @@ Where x and y are conforming numeric lists returns their covariance as a floatin ```python >>> pykx.q.cov(pykx.LongVector([29, 10, 54]), pykx.LongVector([1, 3, 9])) -pykx.FloatAtom(q('47.33333')) +pykx.FloatAtom(pykx.q('47.33333')) ``` ### [deltas](https://code.kx.com/q/ref/deltas/) @@ -1331,7 +1333,7 @@ Where x is a numeric or temporal vector, returns differences between consecutive ```python >>> pykx.q.deltas(pykx.LongVector([1, 4, 9, 16])) -pykx.LongVector(q('1 3 5 7')) +pykx.LongVector(pykx.q('1 3 5 7')) ``` ### [dev](https://code.kx.com/q/ref/dev/) @@ -1340,7 +1342,7 @@ Standard deviation. ```python >>> pykx.q.dev(pykx.LongVector([10, 343, 232, 55])) -pykx.FloatAtom(q('134.3484')) +pykx.FloatAtom(pykx.q('134.3484')) ``` ### [div](https://code.kx.com/q/ref/div/) @@ -1349,7 +1351,7 @@ Integer division. ```python >>> pykx.q.div(7, 3) -pykx.LongAtom(q('2')) +pykx.LongAtom(pykx.q('2')) ``` ### [ema](https://code.kx.com/q/ref/ema/) @@ -1358,7 +1360,7 @@ The cosine of x, taken to be in radians. The result is between -1 and 1, or null ```python >>> pykx.q.ema(0.5, [1, 2, 3, 4, 5]) -pykx.FloatVector(q('1 1.5 2.25 3.125 4.0625')) +pykx.FloatVector(pykx.q('1 1.5 2.25 3.125 4.0625')) ``` ### [exp](https://code.kx.com/q/ref/exp/) @@ -1367,7 +1369,7 @@ Raise *e* to a power. ```python >>> pykx.q.exp(1) -pykx.FloatAtom(q('2.718282')) +pykx.FloatAtom(pykx.q('2.718282')) ``` ### [floor](https://code.kx.com/q/ref/floor/) @@ -1376,7 +1378,7 @@ Round down. ```python >>> pykx.q.floor([-2.7, -1.1, 0, 1.1, 2.7]) -pykx.LongVector(q('-3 -2 0 1 2')) +pykx.LongVector(pykx.q('-3 -2 0 1 2')) ``` ### [inv](https://code.kx.com/q/ref/inv/) @@ -1385,13 +1387,13 @@ Matrix inverse. ```python >>> a = pykx.q('3 3# 2 4 8 3 5 6 0 7 1f') -pykx.List(q(' +pykx.List(pykx.q(' 2 4 8 3 5 6 0 7 1 ')) >>> pykx.q.inv(a) -pykx.List(q(' +pykx.List(pykx.q(' -0.4512195 0.6341463 -0.195122 -0.03658537 0.02439024 0.1463415 0.2560976 -0.1707317 -0.02439024 @@ -1404,7 +1406,7 @@ Natural logarithm. ```python >>> pykx.q.log([1, 2, 3]) -pykx.FloatVector(q('0 0.6931472 1.098612')) +pykx.FloatVector(pykx.q('0 0.6931472 1.098612')) ``` ### [lsq](https://code.kx.com/q/ref/lsq/) @@ -1413,20 +1415,20 @@ Least squares, matrix divide. ```python >>> a = pykx.q('1f+3 4#til 12') -pykx.List(q(' +pykx.List(pykx.q(' 1 2 3 4 5 6 7 8 9 10 11 12 ')) >>> b = pykx.q('4 4#2 7 -2 5 5 3 6 1 -2 5 2 7 5 0 3 4f') -pykx.List(q(' +pykx.List(pykx.q(' 2 7 -2 5 5 3 6 1 -2 5 2 7 5 0 3 4 ')) >>> pykx.q.lsq(a, b) -pykx.List(q(' +pykx.List(pykx.q(' -0.1233333 0.16 0.4766667 0.28 0.07666667 0.6933333 0.6766667 0.5466667 0.2766667 1.226667 0.8766667 0.8133333 @@ -1439,7 +1441,7 @@ Moving averages. ```python >>> pykx.q.mavg(3, [1, 2, 3, 5, 7, 10]) -pykx.FloatVector(q('1 1.5 2 3.333333 5 7.333333')) +pykx.FloatVector(pykx.q('1 1.5 2 3.333333 5 7.333333')) ``` ### [max](https://code.kx.com/q/ref/max/) @@ -1448,7 +1450,7 @@ Maximum. ```python >>> pykx.q.max([0, 7, 2, 4 , 1, 3]) -pykx.LongAtom(q('7')) +pykx.LongAtom(pykx.q('7')) ``` ### [maxs](https://code.kx.com/q/ref/max/#maxs) @@ -1457,7 +1459,7 @@ Maximums. ```python >>> pykx.q.maxs([1, 2, 5, 4, 7, 1, 2]) -pykx.LongVector(q('1 2 5 5 7 7 7')) +pykx.LongVector(pykx.q('1 2 5 5 7 7 7')) ``` ### [mdev](https://code.kx.com/q/ref/dev/#mdev) @@ -1466,7 +1468,7 @@ Moving deviations. ```python >>> pykx.q.mdev(3, [1, 2, 5, 4, 7, 1, 2]) -pykx.FloatVector(q('0 0.5 1.699673 1.247219 1.247219 2.44949 2.624669')) +pykx.FloatVector(pykx.q('0 0.5 1.699673 1.247219 1.247219 2.44949 2.624669')) ``` ### [med](https://code.kx.com/q/ref/med/) @@ -1475,7 +1477,7 @@ Median. ```python >>> pykx.q.med([1, 2, 3, 4, 4, 1, 2, 4, 5]) -pykx.FloatAtom(q('3f')) +pykx.FloatAtom(pykx.q('3f')) ``` ### [min](https://code.kx.com/q/ref/min/) @@ -1484,7 +1486,7 @@ Minimum. ```python >>> pykx.q.min([7, 5, 2, 4, 6, 5, 1, 4]) -pykx.LongAtom(q('1')) +pykx.LongAtom(pykx.q('1')) ``` ### [mins](https://code.kx.com/q/ref/min/#mins) @@ -1493,7 +1495,7 @@ Minimums. ```python >>> pykx.q.mins([7, 5, 2, 4, 6, 5, 1, 4]) -pykx.LongVector(q('7 5 2 2 2 2 1 1')) +pykx.LongVector(pykx.q('7 5 2 2 2 2 1 1')) ``` ### [mmax](https://code.kx.com/q/ref/max/#mmax) @@ -1502,7 +1504,7 @@ Moving maximums. ```python >>> pykx.q.mmax(4, [7, 5, 2, 4, 6, 5, 1, 4]) -pykx.LongVector(q('7 7 7 7 6 6 6 6')) +pykx.LongVector(pykx.q('7 7 7 7 6 6 6 6')) ``` ### [mmin](https://code.kx.com/q/ref/min/#mmin) @@ -1511,7 +1513,7 @@ Moving minimums. ```python >>> pykx.q.mmin(4, pykx.LongVector([7, 5, 2, 4, 6, 5, 1, 4])) -pykx.LongVector(q('7 5 2 2 2 2 1 1')) +pykx.LongVector(pykx.q('7 5 2 2 2 2 1 1')) ``` ### [mmu](https://code.kx.com/q/ref/mmu/) @@ -1521,20 +1523,20 @@ Matrix multiply, dot product. ```python >>> a = pykx.q('2 4#2 4 8 3 5 6 0 7f') >>> a -pykx.List(q(' +pykx.List(pykx.q(' 2 4 8 3 5 6 0 7 ')) >>> b = pykx.q('4 3#"f"$til 12') >>> b -pykx.List(q(' +pykx.List(pykx.q(' 0 1 2 3 4 5 6 7 8 9 10 11 ')) >>> pykx.q.mmu(a, b) -pykx.List(q(' +pykx.List(pykx.q(' 87 104 121 81 99 117 ')) @@ -1546,7 +1548,7 @@ Modulus. ```python >>> pykx.q.mod([1, 2, 3, 4, 5, 6, 7], 4) -pykx.LongVector(q('1 2 3 0 1 2 3')) +pykx.LongVector(pykx.q('1 2 3 0 1 2 3')) ``` ### [msum](https://code.kx.com/q/ref/sum/#msum) @@ -1555,7 +1557,7 @@ Moving sums. ```python >>> pykx.q.msum(3, [1, 2, 3, 4, 5, 6, 7]) -pykx.LongVector(q('1 3 6 9 12 15 18')) +pykx.LongVector(pykx.q('1 3 6 9 12 15 18')) ``` ### [neg](https://code.kx.com/q/ref/neg/) @@ -1564,7 +1566,7 @@ Negate. ```python >>> pykx.q.neg([2, 0, -1, 3, -5]) -pykx.LongVector(q('-2 0 1 -3 5')) +pykx.LongVector(pykx.q('-2 0 1 -3 5')) ``` ### [prd](https://code.kx.com/q/ref/prd/) @@ -1573,7 +1575,7 @@ Product. ```python >>> pykx.q.prd([1, 2, 3, 4, 5]) -pykx.LongAtom(q('120')) +pykx.LongAtom(pykx.q('120')) ``` ### [prds](https://code.kx.com/q/ref/prd/#prds) @@ -1582,7 +1584,7 @@ Cumulative products. ```python >>> pykx.q.prds([1, 2, 3, 4, 5]) -pykx.LongVector(q('1 2 6 24 120')) +pykx.LongVector(pykx.q('1 2 6 24 120')) ``` ### [rand](https://code.kx.com/q/ref/rand/) @@ -1591,7 +1593,7 @@ Pick randomly. ```python >>> pykx.q.rand([1, 2, 3, 4, 5]) -pykx.LongAtom(q('2')) +pykx.LongAtom(pykx.q('2')) ``` ### [ratios](https://code.kx.com/q/ref/ratios/) @@ -1600,7 +1602,7 @@ Ratios between items. ```python >>> pykx.q.ratios([1, 2, 3, 4, 5]) -pykx.FloatVector(q('0n 2 1.5 1.333333 1.25')) +pykx.FloatVector(pykx.q('1 2 1.5 1.333333 1.25')) ``` ### [reciprocal](https://code.kx.com/q/ref/reciprocal/) @@ -1609,7 +1611,7 @@ Reciprocal of a number. ```python >>> pykx.q.reciprocal([1, 0, 3]) -pykx.FloatVector(q('1 0w 0.3333333')) +pykx.FloatVector(pykx.q('1 0w 0.3333333')) ``` ### [scov](https://code.kx.com/q/ref/cov/#scov) @@ -1618,7 +1620,7 @@ Sample covariance. ```python >>> pykx.q.scov(pykx.LongVector([2, 3, 5, 7]), pykx.LongVector([4, 3, 0, 2])) -pykx.FloatAtom(q('-2.416667')) +pykx.FloatAtom(pykx.q('-2.416667')) ``` ### [sdev](https://code.kx.com/q/ref/dev/#sdev) @@ -1627,7 +1629,7 @@ Sample standard deviation. ```python >>> pykx.q.sdev(pykx.LongVector([10, 343, 232, 55])) -pykx.FloatAtom(q('155.1322')) +pykx.FloatAtom(pykx.q('155.1322')) ``` ### [signum](https://code.kx.com/q/ref/signum/) @@ -1640,7 +1642,7 @@ Where x (or its underlying value for temporals) is ```python >>> pykx.q.signum([-2, 0, 1, 3]) -pykx.IntVector(q('-1 0 1 1i')) +pykx.IntVector(pykx.q('-1 0 1 1i')) ``` ### [sin](https://code.kx.com/q/ref/sin/) @@ -1649,7 +1651,7 @@ Sine. ```python >>> pykx.q.sin(0.5) -pykx.FloatAtom(q('0.4794255')) +pykx.FloatAtom(pykx.q('0.4794255')) ``` ### [sqrt](https://code.kx.com/q/ref/sqrt/) @@ -1658,7 +1660,7 @@ Square root. ```python >>> pykx.q.sqrt([-1, 0, 25, 50]) -pykx.FloatVector(q('0n 0 5 7.071068')) +pykx.FloatVector(pykx.q('0n 0 5 7.071068')) ``` ### [sum](https://code.kx.com/q/ref/sum/) @@ -1667,7 +1669,7 @@ Total. ```python >>> pykx.q.sum(pykx.LongVector([2, 3, 5, 7])) -pykx.LongAtom(q('17')) +pykx.LongAtom(pykx.q('17')) ``` ### [sums](https://code.kx.com/q/ref/sum/#sums) @@ -1676,7 +1678,7 @@ Cumulative total. ```python >>> pykx.q.sums(pykx.LongVector([2, 3, 5, 7])) -pykx.LongVector(q('2 5 10 17')) +pykx.LongVector(pykx.q('2 5 10 17')) ``` ### [svar](https://code.kx.com/q/ref/var/#svar) @@ -1685,7 +1687,7 @@ Sample variance. ```python >>> pykx.q.svar(pykx.LongVector([2, 3, 5, 7])) -pykx.FloatAtom(q('4.916667')) +pykx.FloatAtom(pykx.q('4.916667')) ``` ### [tan](https://code.kx.com/q/ref/tan/) @@ -1694,7 +1696,7 @@ Tangent. ```python >>> pykx.q.tan(0.5) -pykx.FloatAtom(q('0.5463025')) +pykx.FloatAtom(pykx.q('0.5463025')) ``` ### [var](https://code.kx.com/q/ref/var/) @@ -1703,7 +1705,7 @@ Variance. ```python >>> pykx.q.var(pykx.LongVector([2, 3, 5, 7])) -pykx.FloatAtom(q('3.6875')) +pykx.FloatAtom(pykx.q('3.6875')) ``` ### [wavg](https://code.kx.com/q/ref/avg/#wavg) @@ -1712,7 +1714,7 @@ Weighted average. ```python >>> pykx.q.wavg([2, 3, 4], [1, 2 ,4]) -pykx.FloatAtom(q('2.666667')) +pykx.FloatAtom(pykx.q('2.666667')) ``` ### [within](https://code.kx.com/q/ref/within/) @@ -1721,7 +1723,7 @@ Check bounds. ```python >>> pykx.q.within([1, 3, 10, 6, 4], [2, 6]) -pykx.BooleanVector(q('01011b')) +pykx.BooleanVector(pykx.q('01011b')) ``` ### [wsum](https://code.kx.com/q/ref/sum/#wsum) @@ -1730,7 +1732,7 @@ Weighted sum. ```python >>> pykx.q.wsum([2, 3, 4], [1, 2, 4]) # equivalent to 2 * 1 + 3 * 2 + 4 * 4 -pykx.LongAtom(q('24')) +pykx.FloatAtom(pykx.q('24f')) ``` ### [xexp](https://code.kx.com/q/ref/exp/#xepx) @@ -1739,7 +1741,7 @@ Raise x to a power. ```python >>> pykx.q.xexp(2, 8) -pykx.FloatAtom(q('256f')) +pykx.FloatAtom(pykx.q('256f')) ``` ### [xlog](https://code.kx.com/q/ref/log/#xlog) @@ -1748,7 +1750,7 @@ Logarithm base x. ```python >>> pykx.q.xlog(2, 8) -pykx.FloatAtom(q('3f')) +pykx.FloatAtom(pykx.q('3f')) ``` ## Meta @@ -1768,9 +1770,9 @@ The possible attributes are: ```python >>> pykx.q.attr([1,2,3]) -pykx.SymbolAtom(q('`')) +pykx.SymbolAtom(pykx.q('`')) >>> pykx.q.attr(pykx.q('asc 1 2 3')) -pykx.SymbolAtom(q('`s')) +pykx.SymbolAtom(pykx.q('`s')) ``` ### [null](https://code.kx.com/q/ref/null/) @@ -1779,11 +1781,11 @@ Is null. ```python >>> pykx.q.null(1) -pykx.BooleanAtom(q('0b')) +pykx.BooleanAtom(pykx.q('0b')) >>> pykx.q.null(float('NaN')) -pykx.BooleanAtom(q('1b')) +pykx.BooleanAtom(pykx.q('1b')) >>> pykx.q.null(None) -pykx.BooleanAtom(q('1b')) +pykx.BooleanAtom(pykx.q('1b')) ``` ### [tables](https://code.kx.com/q/ref/tables/) @@ -1793,8 +1795,8 @@ List of tables in a namespace. ```python >>> pykx.q('exampleTable: ([] a: til 10; b: 10?10)') pykx.Identity(pykx.q('::')) ->>> pykx.q('exampleTable: ([] a: til 10; b: 10?10)') -pykx.Table(q(' +>>> pykx.q('exampleTable') +pykx.Table(pykx.q(' a b --- 0 8 @@ -1809,7 +1811,7 @@ a b 9 5 ')) >>> pykx.q.tables('.') -pykx.SymbolVector(q(',`exampleTable')) +pykx.SymbolVector(pykx.q(',`exampleTable')) ``` ### [type](https://code.kx.com/q/ref/type/) @@ -1818,11 +1820,11 @@ Underlying [k type](https://code.kx.com/q/ref/#datatypes) of an [object](../pykx ```python >>> pykx.q.type(1) -pykx.ShortAtom(q('-7h')) +pykx.ShortAtom(pykx.q('-7h')) >>> pykx.q.type([1, 2, 3]) -pykx.ShortAtom(q('0h')) +pykx.ShortAtom(pykx.q('7h')) >>> pykx.q.type(pykx.LongVector([1, 2, 3])) -pykx.ShortAtom(q('7h')) +pykx.ShortAtom(pykx.q('7h')) ``` ### [view](https://code.kx.com/q/ref/view/) @@ -1833,9 +1835,9 @@ Expression defining a view. >>> pykx.q('v::2+a*3') >>> pykx.q('a:5') >>> pykx.q('v') -pykx.LongAtom(q('17')) +pykx.LongAtom(pykx.q('17')) >>> pykx.q.view('v') -pykx.CharVector(q('"2+a*3"')) +pykx.CharVector(pykx.q('"2+a*3"')) ``` ### [views](https://code.kx.com/q/ref/view/#views) @@ -1846,9 +1848,9 @@ List views defined in the default namespace. >>> pykx.q('v::2+a*3') >>> pykx.q('a:5') >>> pykx.q('v') -pykx.LongAtom(q('17')) +pykx.LongAtom(pykx.q('17')) >>> pykx.q.views() -pykx.SymbolVector(q(',`v')) +pykx.SymbolVector(pykx.q(',`v')) ``` ## Queries @@ -1862,7 +1864,7 @@ Apply an aggregate to groups. pykx.LongVector(pykx.q('4 9 2 7 0 1 9 2 1 8')) >>> group = pykx.SymbolVector(['a', 'b', 'a', 'b', 'c', 'd', 'c', 'd', 'd', 'c']) pykx.SymbolVector(pykx.q('`a`b`a`b`c`d`c`d`d`c')) ->>> >>> pykx.q.fby(pykx.q('(sum; data)'), group) +>>> pykx.q.fby(pykx.q('(sum; data)'), group) pykx.LongVector(pykx.q('6 16 6 16 17 4 17 4 4 17')) ``` @@ -1874,7 +1876,7 @@ Ascending sort. ```python >>> pykx.q.asc([4, 2, 5, 1, 0]) -pykx.LongVector(q('`s#0 1 2 4 5')) +pykx.LongVector(pykx.q('`s#0 1 2 4 5')) ``` ### [bin](https://code.kx.com/q/ref/bin/) @@ -1883,9 +1885,9 @@ Binary search. ```python >>> pykx.q.bin([0, 2, 4, 6, 8, 10], 5) -pykx.LongAtom(q('2')) +pykx.LongAtom(pykx.q('2')) >>> pykx.q.bin([0, 2, 4, 6, 8, 10], [-10, 0, 4, 5, 6, 20]) -pykx.LongVector(q('-1 0 2 2 3 5')) +pykx.LongVector(pykx.q('-1 0 2 2 3 5')) ``` ### [binr](https://code.kx.com/q/ref/bin/#binr) @@ -1894,9 +1896,9 @@ Binary search right. ```python >>> pykx.q.binr([0, 2, 4, 6, 8, 10], 5) -pykx.LongAtom(q('3')) +pykx.LongAtom(pykx.q('3')) >>> pykx.q.binr([0, 2, 4, 6, 8, 10], [-10, 0, 4, 5, 6, 20]) -pykx.LongVector(q('0 0 2 3 3 6')) +pykx.LongVector(pykx.q('0 0 2 3 3 6')) ``` ### [desc](https://code.kx.com/q/ref/desc/) @@ -1905,7 +1907,7 @@ Descending sort. ```python >>> pykx.q.desc([4, 2, 5, 1, 0]) -pykx.LongVector(q('5 4 2 1 0')) +pykx.LongVector(pykx.q('5 4 2 1 0')) ``` ### [differ](https://code.kx.com/q/ref/differ/) @@ -1914,7 +1916,7 @@ Find where list items change value. ```python >>> pykx.q.differ([1, 1, 2, 3, 4, 4]) -pykx.BooleanVector(q('101110b')) +pykx.BooleanVector(pykx.q('101110b')) ``` ### [distinct](https://code.kx.com/q/ref/distinct/) @@ -1923,7 +1925,7 @@ Unique items of a list. ```python >>> pykx.q.distinct([1, 3, 1, 4, 5, 1, 2, 3]) -pykx.LongVector(q('1 3 4 5 2')) +pykx.LongVector(pykx.q('1 3 4 5 2')) ``` ### [iasc](https://code.kx.com/q/ref/asc/#iasc) @@ -1932,7 +1934,7 @@ Ascending grade. ```python >>> pykx.q.iasc([4, 2, 5, 1, 0]) -pykx.LongVector(q('4 3 1 0 2')) +pykx.LongVector(pykx.q('4 3 1 0 2')) ``` ### [idesc](https://code.kx.com/q/ref/desc/#idesc) @@ -1941,7 +1943,7 @@ Descending grade. ```python >>> pykx.q.idesc([4, 2, 5, 1, 0]) -pykx.LongVector(q('2 0 1 3 4')) +pykx.LongVector(pykx.q('2 0 1 3 4')) ``` ### [rank](https://code.kx.com/q/ref/rank/) @@ -1952,9 +1954,9 @@ Where x is a list or dictionary, returns for each item in x the index of where i ```python >>> pykx.q.rank([4, 2, 5, 1, 0]) -pykx.LongVector(q('3 2 4 1 0')) +pykx.LongVector(pykx.q('3 2 4 1 0')) >>> pykx.q.rank({'c': 3, 'a': 4, 'b': 1}) -pykx.LongVector(q('2 0 1')) +pykx.LongVector(pykx.q('2 0 1')) ``` ### [xbar](https://code.kx.com/q/ref/xbar/) @@ -1963,13 +1965,13 @@ Round y down to the nearest multiple of x. ```python >>> pykx.q.xbar(5, 3) -pykx.LongAtom(q('0')) +pykx.LongAtom(pykx.q('0')) >>> pykx.q.xbar(5, 5) -pykx.LongAtom(q('5')) +pykx.LongAtom(pykx.q('5')) >>> pykx.q.xbar(5, 7) -pykx.LongAtom(q('5')) +pykx.LongAtom(pykx.q('5')) >>> pykx.q.xbar(3, range(16)) -pykx.LongVector(q('0 0 0 3 3 3 6 6 6 9 9 9 12 12 12 15')) +pykx.LongVector(pykx.q('0 0 0 3 3 3 6 6 6 9 9 9 12 12 12 15')) ``` ### [xrank](https://code.kx.com/q/ref/xrank/) @@ -1978,9 +1980,9 @@ Group by value. ```python >>> pykx.q.xrank(3, range(6)) -pykx.LongVector(q('0 0 1 1 2 2')) +pykx.LongVector(pykx.q('0 0 1 1 2 2')) >>> pykx.q.xrank(4, range(9)) -pykx.LongVector(q('0 0 0 1 1 2 2 3 3')) +pykx.LongVector(pykx.q('0 0 0 1 1 2 2 3 3')) ``` ## Table @@ -1993,12 +1995,12 @@ Column names of a table. >>> import pandas as pd >>> import numpy as np >>> df = pd.DataFrame({ -... 'time': numpy.array([1, 2, 3, 4], dtype='timedelta64[s]'), +... 'time': np.array([1, 2, 3, 4], dtype='timedelta64[s]'), ... 'sym':['a', 'a', 'b', 'b'], ... 'p': pykx.LongVector([2, 4, 6, 8]) ... }) >>> pykx.q.cols(df) -pykx.SymbolVector(q('`time`sym`p')) +pykx.SymbolVector(pykx.q('`time`sym`p')) ``` ### [csv](https://code.kx.com/q/ref/csv/) @@ -2009,7 +2011,7 @@ A synonym for "," for use in preparing text for CSV files, or reading them. ```python >>> pykx.q.csv -pykx.CharAtom(q('","')) +pykx.CharAtom(pykx.q('","')) ``` ### [fkeys](https://code.kx.com/q/ref/fkeys/) @@ -2018,11 +2020,11 @@ Foreign-key columns of a table. ```python >>> pykx.q('f:([x:1 2 3]y:10 20 30)') -pykx.Identity(q('::')) +pykx.Identity(pykx.q('::')) >>> pykx.q('t: ([]a:`f$2 2 2; b: 0; c: `f$1 1 1)') -pykx.Identity(q('::')) +pykx.Identity(pykx.q('::')) >>> pykx.q.fkeys('t') -pykx.Dictionary(q(' +pykx.Dictionary(pykx.q(' a| f c| f ')) @@ -2035,7 +2037,7 @@ Insert or append records to a table. ```python >>> pykx.q('t: ([] a: `a`b`c; b: til 3)') >>> pykx.q('t') -pykx.Table(q(' +pykx.Table(pykx.q(' a b --- a 0 @@ -2044,7 +2046,7 @@ c 2 ')) >>> pykx.q.insert('t', ['d', 3]) >>> pykx.q('t') -pykx.Table(q(' +pykx.Table(pykx.q(' a b --- a 0 @@ -2060,7 +2062,7 @@ Where x is a dictionary (or the name of one), returns its keys. ```python >>> pykx.q.key({'a': 1, 'b': 2}) -pykx.SymbolVector(q('`a`b')) +pykx.SymbolVector(pykx.q('`a`b')) ``` ### [keys](https://code.kx.com/q/ref/keys/) @@ -2078,7 +2080,7 @@ idx| x 3 | 6 ')) >>> pykx.q.keys('v') -pykx.SymbolVector(q(',`idx')) +pykx.SymbolVector(pykx.q(',`idx')) ``` ### [meta](https://code.kx.com/q/ref/meta/) @@ -2101,10 +2103,10 @@ Metadata for a table. ... 'p': pykx.LongVector([2, 4, 6, 8]) ... }) >>> pykx.q.meta(df) -pykx.KeyedTable(q(' +pykx.KeyedTable(pykx.q(' c | t f a ----| ----- -time| n +time| v sym | s p | j ')) @@ -2118,14 +2120,14 @@ Where x is a table, in which some cells are lists, but for any row, all lists ar >>> a = pykx.Table([['a', [2, 3], 10], ['b', [5, 6, 7], 20], ['c', [11], 30]], columns=['s', 'x', 'q']) >>> a pykx.Table(pykx.q(' -s x q ------------- -a (2;3) 10 -b (5;6;7) 20 -c ,11 30 +s x q +---------- +a 2 3 10 +b 5 6 7 20 +c ,11 30 ')) >>> pykx.q.ungroup(a) -pykx.Table(q(' +pykx.Table(pykx.q(' s x q ------- a 2 10 @@ -2145,7 +2147,7 @@ Add new records to a table. >>> import pandas as pd >>> df = pd.DataFrame({'sym':['a', 'a', 'b', 'b'], 'p': pykx.LongVector([2, 4, 6, 8])}) >>> pykx.Table(df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ----- a 2 @@ -2173,7 +2175,7 @@ Sort a table in ascending order of specified columns. >>> import pandas as pd >>> df = pd.DataFrame({'sym':['a', 'a', 'b', 'b', 'c', 'c'], 'p': pykx.LongVector([10, 4, 6, 2, 0, 8])}) >>> pykx.Table(df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ a 10 @@ -2184,7 +2186,7 @@ c 0 c 8 ')) >>> pykx.q.xasc('p', df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ c 0 @@ -2204,7 +2206,7 @@ Rename table columns. >>> import pandas as pd >>> df = pd.DataFrame({'sym':['a', 'a', 'b', 'b', 'c', 'c'], 'p': pykx.LongVector([10, 4, 6, 2, 0, 8])}) >>> pykx.Table(df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ a 10 @@ -2215,7 +2217,7 @@ c 0 c 8 ')) >>> pykx.q.xcol(pykx.SymbolVector(['Sym', 'Qty']), df) -pykx.Table(q(' +pykx.Table(pykx.q(' Sym Qty ------- a 10 @@ -2226,7 +2228,7 @@ c 0 c 8 ')) >>> pykx.q.xcol({'p': 'Qty'}, df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym Qty ------- a 10 @@ -2251,22 +2253,22 @@ Reorder table columns. ... 'p': pykx.LongVector([2, 4, 6, 8]) ... }) >>> pykx.Table(df) -pykx.Table(q(' -time sym p --------------------------- -0D00:00:01.000000000 a 2 -0D00:00:02.000000000 a 4 -0D00:00:03.000000000 b 6 -0D00:00:04.000000000 b 8 +pykx.Table(pykx.q(' +time sym p +-------------- +00:00:01 a 2 +00:00:02 a 4 +00:00:03 b 6 +00:00:04 b 8 ')) >>> pykx.q.xcols(pykx.SymbolVector(['p', 'sym', 'time']), df) -pykx.Table(q(' +pykx.Table(pykx.q(' p sym time --------------------------- -2 a 0D00:00:01.000000000 -4 a 0D00:00:02.000000000 -6 b 0D00:00:03.000000000 -8 b 0D00:00:04.000000000 +-------------- +2 a 00:00:01 +4 a 00:00:02 +6 b 00:00:03 +8 b 00:00:04 ')) ``` @@ -2278,7 +2280,7 @@ Sorts a table in descending order of specified columns. The sort is by the first >>> import pandas as pd >>> df = pd.DataFrame({'sym':['a', 'a', 'b', 'b', 'c', 'c'], 'p': pykx.LongVector([10, 4, 6, 2, 0, 8])}) >>> pykx.Table(df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ a 10 @@ -2289,7 +2291,7 @@ c 0 c 8 ')) >>> pykx.q.xdesc('p', df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ a 10 @@ -2309,7 +2311,7 @@ Groups a table by values in selected columns. >>> import pandas as pd >>> df = pd.DataFrame({'sym':['a', 'a', 'b', 'b', 'c', 'c'], 'p': pykx.LongVector([10, 4, 6, 2, 0, 8])}) >>> pykx.Table(df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ a 10 @@ -2320,7 +2322,7 @@ c 0 c 8 ')) >>> pykx.q.xgroup('sym', df) -pykx.KeyedTable(q(' +pykx.KeyedTable(pykx.q(' sym| p ---| ---- a | 10 4 @@ -2337,7 +2339,7 @@ Set specified columns as primary keys of a table. >>> import pandas as pd >>> df = pd.DataFrame({'sym':['a', 'a', 'b', 'b', 'c', 'c'], 'p': pykx.LongVector([10, 4, 6, 2, 0, 8])}) >>> pykx.Table(df) -pykx.Table(q(' +pykx.Table(pykx.q(' sym p ------ a 10 @@ -2348,7 +2350,7 @@ c 0 c 8 ')) >>> pykx.q.xkey('p', df) -pykx.KeyedTable(q(' +pykx.KeyedTable(pykx.q(' p | sym --| --- 10| a @@ -2368,11 +2370,11 @@ Whether text matches a pattern. ```python >>> pykx.q.like('quick', b'qu?ck') -pykx.BooleanAtom(q('1b')) +pykx.BooleanAtom(pykx.q('1b')) >>> pykx.q.like('brown', b'br[ao]wn') -pykx.BooleanAtom(q('1b')) +pykx.BooleanAtom(pykx.q('1b')) >>> pykx.q.like('quick', b'quickish') -pykx.BooleanAtom(q('0b')) +pykx.BooleanAtom(pykx.q('0b')) ``` ### [lower](https://code.kx.com/q/ref/lower/) @@ -2381,9 +2383,9 @@ Shift case to lower case. ```python >>> pykx.q.lower('HELLO') -pykx.SymbolAtom(q('`hello')) +pykx.SymbolAtom(pykx.q('`hello')) >>> pykx.q.lower(b'HELLO') -pykx.CharVector(q('"hello"')) +pykx.CharVector(pykx.q('"hello"')) ``` ### [ltrim](https://code.kx.com/q/ref/trim/#ltrim) @@ -2392,7 +2394,7 @@ Remove leading nulls from a list. ```python >>> pykx.q.ltrim(b' pykx ') -pykx.CharVector(q('"pykx "')) +pykx.CharVector(pykx.q('"pykx "')) ``` ### [md5](https://code.kx.com/q/ref/md5/) @@ -2401,7 +2403,7 @@ Message digest hash. ```python >>> pykx.q.md5(b'pykx') -pykx.ByteVector(q('0xfba0532951f022133f8e8b14b6ddfced')) +pykx.ByteVector(pykx.q('0xfba0532951f022133f8e8b14b6ddfced')) ``` ### [rtrim](https://code.kx.com/q/ref/trim/#rtrim) @@ -2410,7 +2412,7 @@ Remove trailing nulls from a list. ```python >>> pykx.q.rtrim(b' pykx ') -pykx.CharVector(q('" pykx"')) +pykx.CharVector(pykx.q('" pykx"')) ``` ### [ss](https://code.kx.com/q/ref/ss/) @@ -2419,7 +2421,7 @@ String search. ```python >>> pykx.q.ss(b'a cat and a dog', b'a') -pykx.LongVector(q('0 3 6 10')) +pykx.LongVector(pykx.q('0 3 6 10')) ``` ### [ssr](https://code.kx.com/q/ref/ss/#ssr) @@ -2428,7 +2430,7 @@ String search and replace. ```python >>> pykx.q.ssr(b'toronto ontario', b'ont', b'x') -pykx.CharVector(q('"torxo xario"')) +pykx.CharVector(pykx.q('"torxo xario"')) ``` ### [string](https://code.kx.com/q/ref/string/) @@ -2437,9 +2439,9 @@ Cast to string. ```python >>> pykx.q.string(2) -pykx.CharVector(q(',"2"')) +pykx.CharVector(pykx.q(',"2"')) >>> pykx.q.string([1, 2, 3, 4, 5]) -pykx.List(q(' +pykx.List(pykx.q(' ,"1" ,"2" ,"3" @@ -2454,7 +2456,7 @@ Remove leading and trailing nulls from a list. ```python >>> pykx.q.trim(b' pykx ') -pykx.CharVector(q('"pykx"')) +pykx.CharVector(pykx.q('"pykx"')) ``` ### [upper](https://code.kx.com/q/ref/lower/#upper) @@ -2463,9 +2465,9 @@ Shift case to upper case. ```python >>> pykx.q.upper('hello') -pykx.SymbolAtom(q('`HELLO')) +pykx.SymbolAtom(pykx.q('`HELLO')) >>> pykx.q.upper(b'hello') -pykx.CharVector(q('"HELLO"')) +pykx.CharVector(pykx.q('"HELLO"')) ``` ## Operators @@ -2516,7 +2518,7 @@ Example: Coalesce two keyed tables one containing nulls ```python ->> tab1 = kx.Table(data={ +>>> tab1 = kx.Table(data={ ... 'x': kx.q.til(10), ... 'y': kx.random.random(10, 10.0) ... }).set_index('x') diff --git a/docs/api/reimporting.md b/docs/api/reimporting.md index 96f3dd6..6a77650 100644 --- a/docs/api/reimporting.md +++ b/docs/api/reimporting.md @@ -7,4 +7,15 @@ tags: reimport, pykx, import --- # Reimporting +!!! note "Usually not required" + + KDB-X Python now automatically manages the environment variables needed to reimport it, so in + most cases you can start a subprocess that imports KDB-X Python (or, for a KDB-X 5.0 `q` + process, loads `pykx.q`) directly, without `PyKXReimport`. + + `PyKXReimport` is **still required** when starting a **kdb+ 4.0/4.1** `q` subprocess from a + Python process that has imported KDB-X Python. Alternatively, set `PYKX_IGNORE_QHOME=true` + so `QHOME` is not redirected in the first place. + Refer to [QHOME, symlinking and subprocesses](../user-guide/configuration.md#qhome-symlinking-and-subprocesses) for details. + ::: pykx.reimporter diff --git a/docs/examples/interface-overview.ipynb b/docs/examples/interface-overview.ipynb deleted file mode 100644 index 0fced04..0000000 --- a/docs/examples/interface-overview.ipynb +++ /dev/null @@ -1,1149 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# KDB-X Python introduction notebook\n", - "\n", - "_The purpose of this notebook is to introduce you to KDB-X Python capabilities and functionality._\n", - "\n", - "For the best experience, visit [what is KDB-X Python](../getting-started/what_is_pykx.html) and the [quickstart guide](../getting-started/quickstart.html) first.\n", - "\n", - "To follow along, we recommend to download the notebook. \n", - "\n", - "Now let's go through the following sections:\n", - "\n", - "1. [Import KDB-X Python](#1-import-kdb-x-python)\n", - "1. [Basic KDB-X Python data structures](#2-basic-pykx-data-structures)\n", - "1. [Access and create `pykx` objects](#3-access-and-create-pykx-objects)\n", - "1. [Run analytics on `pykx` objects](#4-run-analytics-on-pykx-objects)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Import KDB-X Python\n", - "\n", - "To access KDB-X Python and its functions, import it in your Python code as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [ - "hide_code" - ] - }, - "outputs": [], - "source": [ - "import os\n", - "os.environ['PYKX_IGNORE_QHOME'] = '1' # Ignore symlinking KDB-X Python q libraries to QHOME\n", - "os.environ['PYKX_Q_LOADED_MARKER'] = '' # Only used here for running Notebook under mkdocs-jupyter during document generation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pykx as kx\n", - "kx.q.system.console_size = [10, 80]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Tip: We recommend to always use `import pykx as kx`. The shortened import name `kx` makes the code more readabile and is standard for the KDB-X Python library. \n", - "\n", - "Below we load additional libraries used through this notebook:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Basic `pykx` data structures\n", - "\n", - "Central to your interaction with KDB-X Python are the data types supported by the library. KDB-X Python (`pykx`) is built atop the `q` programming language. This provides small footprint data structures for analytic calculations and the creation of highly-performant databases. The types we show below are generated from Python-equivalent types.\n", - "\n", - "This section describes the basic elements in the KDB-X Python library and explains why/how they are different:\n", - "\n", - "- 2.1 [Atom](#21-atom)\n", - "- 2.2 [Vector](#22-vector)\n", - "- 2.3 [List](#23-list)\n", - "- 2.4 [Dictionary](#24-dictionary)\n", - "- 2.5 [Table](#25-table)\n", - "- 2.6 [Other data types](#26-other-data-types)\n", - "\n", - "\n", - "### 2.1 Atom\n", - "\n", - "In KDB-X Python, an **atom** is a single irreducible value of a specific type. For example, you may come across `pykx.FloatAtom` or `pykx.DateAtom` objects which may have been generated as follows, from an equivalent Pythonic representation. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.FloatAtom(1.0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import date\n", - "kx.DateAtom(date(2020, 1, 1))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.2 Vector\n", - "\n", - "Like `pykx` atoms, `pykx` vectors are a data structure with multiple elements of a single type. These objects in `pykx`, along with lists described below, form the basis for most of the other important data structures that you will encounter including dictionaries and tables.\n", - "\n", - "Vector objects provide significant benefits when applying analytics over Python lists. Like Numpy, `pykx` gains from the underlying speed of its analytic engine when operating on these strictly typed objects.\n", - "\n", - "Vector type objects are always 1-D and are/can be indexed along a single axis.\n", - "\n", - "In the following example, we create `pykx` vectors from common Python equivalent `numpy` and `pandas` objects:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.IntVector(np.array([1, 2, 3, 4], dtype=np.int32))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.toq(pd.Series([1, 2, 3, 4]))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.3 List\n", - "\n", - "A `pykx.List` is an untyped vector object. Unlike vectors which are optimised for the performance of analytics, lists are mostly used for storing reference information or matrix data.\n", - "\n", - "Unlike vector objects which are 1-D in shape, lists can be ragged N-Dimensional objects. This makes them useful for storing complex data structures, but limits their performance when dealing with data-access/data modification tasks." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.List([[1, 2, 3], [1.0, 1.1, 1.2], ['a', 'b', 'c']])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.4 Dictionary\n", - "\n", - "A `pykx.Dictionary` is a mapping between a direct key-value association. The list of keys and values to which they are associated must have the same count. While it can be considered as a key-value pair, it's physically stored as a pair of lists." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.Dictionary({'x': [1, 2, 3], 'x1': np.array([1, 2, 3])})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.5 Table\n", - "\n", - "`pykx.Table` objects are a first-class typed entity which lives in memory. They're a collection of named columns implemented as a dictionary. This mapping construct means that `pykx` tables are column oriented. This makes analytic operations on columns much faster than for a relational database equivalent.\n", - "\n", - "`pykx` tables come in many forms, but the key table types are as follows:\n", - "\n", - " - `pykx.Table` \n", - " - `pykx.KeyedTable`\n", - " - `pykx.SplayedTable`\n", - " - `pykx.PartitionedTable`\n", - "\n", - "In this section we exemplify the first two, which are the in-memory data table types.\n", - "\n", - "#### pykx.Table" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(kx.Table([[1, 2, 'a'], [2, 3, 'b'], [3, 4, 'c']], columns = ['col1', 'col2', 'col3']))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(kx.Table(data = {'col1': [1, 2, 3], 'col2': [2 , 3, 4], 'col3': ['a', 'b', 'c']}))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### pykx.KeyedTable" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.Table([[1, 2, 'a'], [2, 3, 'b'], [3, 4, 'c']],\n", - " columns = ['col1', 'col2', 'col3'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.Table(data = {\n", - " 'col1': [1, 2, 3],\n", - " 'col2': [2 , 3, 4],\n", - " 'col3': ['a', 'b', 'c']})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### `pykx.KeyedTable`\n", - "\n", - "[pykx.KeyedTable](../api/pykx-q-data/wrappers.html#pykx.wrappers.KeyedTable)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.Table(data = {'x': [1, 2, 3], 'x1': [2, 3, 4], 'x2': ['a', 'b', 'c']}\n", - " ).set_index(['x'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2.6 Other data types\n", - "\n", - "Below we outlined some of the important `pykx` data type structures that you will run into through the rest of this notebook.\n", - "\n", - "#### pykx.Lambda\n", - "\n", - "A `pykx.Lambda` is the most basic kind of function within `pykx`. They take between 0 and 8 parameters and are the building blocks for most analytics written by users when interacting with data from KDB-X Python." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pykx_lambda = kx.q('{x+y}')\n", - "type(pykx_lambda)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pykx_lambda(1, 2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### pykx.Projection\n", - "\n", - "Like [functools.partial](https://docs.python.org/3/library/functools.html#functools.partial), functions in `pykx` can have some of their parameters set in advance, resulting in a new function, which is called a projection. When you call this projection, the set parameters are no longer required and cannot be provided.\n", - "\n", - "If the original function had `n` total parameters and `m` provided, the result would be a function (projection) that requires the user to input `n-m` parameters." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "projection = kx.q('{x+y}')(1)\n", - "projection" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "projection(2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Access and create `pykx` objects \n", - "\n", - "Now that you're familiar with the `pykx` object types, let's see how they work in real-world scenarios, such as:\n", - "\n", - "- 3.1 [Create `pykx` objects from Pythonic data types](#31-create-pykx-objects-from-pythonic-data-types)\n", - "- 3.2 [Random data generation](#32-random-data-generation)\n", - "- 3.3 [Run q code to generate data](#33-run-q-code-to-generate-data)\n", - "- 3.4 [Read data from a CSV file](#34-read-data-from-a-csv-file)\n", - "- 3.5 [Query external processes via IPC](#35-query-external-processes-via-ipc)\n", - "\n", - "### 3.1 Create `pykx` objects from Pythonic data types\n", - "\n", - "One of the most common ways to generate `pykx` data is by converting equivalent Pythonic data types. `pykx` natively supports conversions to and from the following common Python data formats:\n", - "\n", - "- Python\n", - "- Numpy\n", - "- Pandas\n", - "- PyArrow\n", - "\n", - "You can generate `pykx` objects by using the `kx.toq` `pykx` function:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pydict = {'a': [1, 2, 3], 'b': ['a', 'b', 'c'], 'c': 2}\n", - "kx.toq(pydict)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nparray = np.array([1, 2, 3, 4], dtype = np.int32)\n", - "kx.toq(nparray)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pdframe = pd.DataFrame(data = {'a':[1, 2, 3], 'b': ['a', 'b', 'c']})\n", - "kx.toq(pdframe)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2 Random data generation\n", - "\n", - "KDB-X Python provides a module to create random data of user-specified `pykx` types or their equivalent Python types. The creation of random data helps in prototyping analytics.\n", - "\n", - "As a first example, generate a list of 1,000,000 random floating-point values between 0 and 1 as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.random.random(1000000, 1.0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you wish to choose values randomly from a list, use the list as the second argument to your function:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.random.random(5, [kx.LongAtom(1), ['a', 'b', 'c'], np.array([1.1, 1.2, 1.3])])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Random data does not only come in 1-Dimensional forms. To create multi-Dimensional `pykx` lists, turn the first argument into a list. The following examples include a `pykx` trick that uses nulls/infinities to generate random data across the full allowable range:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.random.random([2, 5], kx.GUIDAtom.null)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.random.random([2, 3, 4], kx.IntAtom.inf)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, to have consistency over the generated objects, set the seed for the random data generation explicitly. You can complete this globally or for individual function calls:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.random.seed(10)\n", - "kx.random.random(10, 2.0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.random.random(10, 2.0, seed = 10)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.3 Run q code to generate data\n", - "\n", - "KDB-X Python (`pykx`) is an entry point to the vector programming language q. This means that KDB-X Python users can execute q code directly via `pykx` within a Python session, by calling `kx.q`.\n", - "\n", - "For example, to create q data, run the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q('0 1 2 3 4')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q('([idx:desc til 5]col1:til 5;col2:5?1f;col3:5?`2)')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, apply arguments to a user-specified function `x+y`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q('{x+y}', kx.LongAtom(1), kx.LongAtom(2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.4 Read data from a CSV file\n", - "\n", - "A lot of data that you run into for data analysis tasks comes in the form of CSV files. KDB-X Python, like Pandas, provides a CSV reader called via `kx.q.read.csv`. In the next cell we create a CSV that can be read in `pykx`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import csv\n", - "\n", - "with open('pykx.csv', 'w', newline='') as file:\n", - " writer = csv.writer(file)\n", - " field = [\"name\", \"age\", \"height\", \"country\"]\n", - " \n", - " writer.writerow(field)\n", - " writer.writerow([\"Oladele Damilola\", \"40\", \"180.0\", \"Nigeria\"])\n", - " writer.writerow([\"Alina Hricko\", \"23\", \"179.2\", \"Ukraine\"])\n", - " writer.writerow([\"Isabel Walter\", \"50\", \"179.5\", \"United Kingdom\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.read.csv('pykx.csv', types = {'age': kx.LongAtom, 'country': kx.SymbolAtom})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "os.remove('pykx.csv')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.5 Query external processes via IPC\n", - "\n", - "One of the most common usage patterns in organizations with access to data in KDB-X/q is to query data from an external server process infrastructure. For the example below you need to [install q](https://code.kx.com/kdb-x/get_started/kdb-x-install.html).\n", - "\n", - "First, set up a q/KDB-X server. Set it on port 5050 and populate it with some data in the form of a table `tab`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import subprocess\n", - "import time\n", - "\n", - "try:\n", - " with kx.PyKXReimport():\n", - " proc = subprocess.Popen(\n", - " ('q', '-p', '5000')\n", - " )\n", - " time.sleep(2)\n", - "except:\n", - " raise kx.QError('Unable to create q process on port 5000')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once a q process is available, connect to it for synchronous query execution:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "conn = kx.SyncQConnection(port = 5000)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can now run q commands against the q server:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "conn('tab:([]col1:100?`a`b`c;col2:100?1f;col3:100?0Ng)')\n", - "conn('select from tab where col1=`a')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Alternatively, use the `pykx` query API:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "conn.qsql.select('tab', where=((kx.Column('col1')=='a') & (kx.Column('col2')>0.3)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or use the `pykx` context interface to run SQL server side if you have access to it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "conn.sql('SELECT * FROM tab where col2>=0.5')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, shut down the q server used for this demonstration:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "proc.kill()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Run analytics on `pykx` objects\n", - "\n", - "Like many Python libraries (including Numpy and Pandas), KDB-X Python (`pykx`) provides many ways to use its data with analytics that you generated and defined within the library. Let's explore the following:\n", - "\n", - "- 4.1 [Use in-built methods on `pykx` vectors](#41-use-in-built-methods-on-pykx-vectors)\n", - "- 4.2 [Use in-built methods on `pykx` tables](#42-use-in-built-methods-on-pykx-tables)\n", - "- 4.3 [Use `pykx`/q native functions](#43-use-pykxq-native-functions)\n", - "\n", - "\n", - "### 4.1 Use in-built methods on `pykx` Vectors\n", - "\n", - "When you interact with `pykx` vectors, you may wish to gain insights into these objects through the application of basic analytics such as calculation of the `mean`/`median`/`mode` of the vector:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "q_vector = kx.random.random(1000, 10.0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "q_vector.mean()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "q_vector.max()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The above is useful for basic analysis. For bespoke analytics on these vectors, use the `apply` method:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def bespoke_function(x, y):\n", - " return x*y\n", - "\n", - "q_vector.apply(bespoke_function, 5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.2 Use in-built methods on `pykx` tables\n", - "\n", - "In addition to the vector processing capabilities of KDB-X Python, it's important to have the ability to manage tables. Highlighted in depth within the Pandas-Like API documentation [here](../user-guide/advanced/Pandas_API.ipynb), these methods allow you to apply functions and gain insights into your data in a familiar way.\n", - "\n", - "The example below uses combinations of the most used elements of this Table API operating on the following table:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "N = 1000000\n", - "example_table = kx.Table(data = {\n", - " 'sym' : kx.random.random(N, ['a', 'b', 'c']),\n", - " 'col1' : kx.random.random(N, 10.0),\n", - " 'col2' : kx.random.random(N, 20)\n", - " }\n", - ")\n", - "example_table" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can search for and filter data within your tables using `loc` similarly to how this is achieved by Pandas:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "example_table.loc[example_table['sym'] == 'a']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This also happens when retrieving data from a table through the `__get__` method:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "example_table[example_table['sym'] == 'b']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, you can set the index columns of a table. In KDB-X Python, this means converting the table from a `pykx.Table` object to a `pykx.KeyedTable` object:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "example_table.set_index('sym')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or you can apply basic data manipulation operations such as `mean` and `median`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print('mean:')\n", - "display(example_table.mean(numeric_only = True))\n", - "\n", - "print('median:')\n", - "display(example_table.median(numeric_only = True))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, use the `groupby` method to group `pykx` tabular data so you can use it for analytic purposes.\n", - "\n", - "In the first example, let's start by grouping the dataset based on the `sym` column and calculate the `mean` for each column based on their `sym`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "example_table.groupby('sym').mean()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To extend the above `groupby`, consider a more complex example which uses `numpy` to run calculations on the `pykx` data. You will notice later that you can simplify this specific use-case further." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def apply_func(x):\n", - " nparray = x.np()\n", - " return np.sqrt(nparray).mean()\n", - "\n", - "example_table.groupby('sym').apply(apply_func)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For time-series specific joining of data, use `merge_asof` joins. In this example, you have several tables with temporal information namely a `trades` and `quotes` table:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trades = kx.Table(data={\n", - " \"time\": [\n", - " pd.Timestamp(\"2016-05-25 13:30:00.023\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.023\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.030\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.041\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.048\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.049\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.072\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.075\")\n", - " ],\n", - " \"ticker\": [\n", - " \"GOOG\",\n", - " \"MSFT\",\n", - " \"MSFT\",\n", - " \"MSFT\",\n", - " \"GOOG\",\n", - " \"AAPL\",\n", - " \"GOOG\",\n", - " \"MSFT\"\n", - " ],\n", - " \"bid\": [720.50, 51.95, 51.97, 51.99, 720.50, 97.99, 720.50, 52.01],\n", - " \"ask\": [720.93, 51.96, 51.98, 52.00, 720.93, 98.01, 720.88, 52.03]\n", - "})\n", - "quotes = kx.Table(data={\n", - " \"time\": [\n", - " pd.Timestamp(\"2016-05-25 13:30:00.023\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.038\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.048\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.048\"),\n", - " pd.Timestamp(\"2016-05-25 13:30:00.048\")\n", - " ],\n", - " \"ticker\": [\"MSFT\", \"MSFT\", \"GOOG\", \"GOOG\", \"AAPL\"],\n", - " \"price\": [51.95, 51.95, 720.77, 720.92, 98.0],\n", - " \"quantity\": [75, 155, 100, 100, 100]\n", - "})\n", - "\n", - "print('trades:')\n", - "display(trades)\n", - "print('quotes:')\n", - "display(quotes)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When applying the `asof` join, you can additionally use named arguments to make a distinction between the tables that the columns originate from. In this case, suffix with `_trades` and `_quotes`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trades.merge_asof(quotes, on='time', suffixes=('_trades', '_quotes'))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4.3 Use `pykx`/q native functions\n", - "\n", - "While the Pandas-like API and methods provided off `pykx` vectors provides an effective method of applying analytics on `pykx` data, the most efficient and performant way to run analytics on your data is by using `pykx`/q primitives available through the `kx.q` module.\n", - "\n", - "These include functionality for calculating moving averages, asof/window joins, column reversal etc. Now let's see a few examples with how you can use these functions, grouped into the following sections:\n", - "\n", - "- 4.3.1 [Mathematical functions](#431-mathematical-functions)\n", - "- 4.3.2 [Iteration functions](#432-iteration-functions)\n", - "- 4.3.3 [Table functions](#433-table-functions)\n", - "\n", - "#### 4.3.1 Mathematical functions\n", - "\n", - "##### mavg\n", - "\n", - "Calculate a series of average values across a list using a rolling window:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.mavg(10, kx.random.random(10000, 2.0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### cor\n", - "\n", - "Calculate the correlation between two lists:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.cor([1, 2, 3], [2, 3, 4])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.cor(kx.random.random(100, 1.0), kx.random.random(100, 1.0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### prds\n", - "\n", - "Calculate the cumulative product across a supplied list:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.prds([1, 2, 3, 4, 5])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 4.3.2 Iteration functions\n", - "\n", - "##### each\n", - "\n", - "Supplied both as a standalone primitive and as a method for `pykx` Lambdas `each` allows you to pass individual elements of a `pykx` object to a function:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.each(kx.q('{prd x}'), kx.random.random([5, 5], 10.0, seed=10))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q('{prd x}').each(kx.random.random([5, 5], 10.0, seed=10))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 4.3.3 Table functions\n", - "\n", - "##### meta\n", - "\n", - "Retrieve metadata information about a table:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "qtab = kx.Table(data = {\n", - " 'x' : kx.random.random(1000, ['a', 'b', 'c']).grouped(),\n", - " 'y' : kx.random.random(1000, 1.0),\n", - " 'z' : kx.random.random(1000, kx.TimestampAtom.inf)\n", - "})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.meta(qtab)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### xasc\n", - "\n", - "Sort the contents of a specified column in ascending order:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kx.q.xasc('z', qtab)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can find the full list of the functions and some examples of their usage [here](../api/pykx-execution/q.md).\n", - "\n" - ] - } - ], - "metadata": { - "file_extension": ".py()", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - }, - "mimetype": "text/x-python", - "name": "python", - "npconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": 3 - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/examples/interface-overview.md b/docs/examples/interface-overview.md new file mode 100644 index 0000000..10a8312 --- /dev/null +++ b/docs/examples/interface-overview.md @@ -0,0 +1,1031 @@ +--- +title: KDB-X Python Fundamentals +description: Learn KDB-X Python data structures, conversions, table methods, q functions, CSV input, and IPC +last_updated: July 2026 +author: KX Systems, Inc., +keywords: + - KDB-X Python + - pykx + - fundamentals + - q + - data structures + - conversions + - IPC +--- + +# KDB-X Python Fundamentals + +_Learn how to create and convert typed `pykx` objects, analyze vectors and tables, call q functions from Python, and connect to an external q process over IPC._ + +**Estimated time:** 30 minutes + +## Prerequisites + +Before you start: + +- [Install KDB-X Python](../getting-started/installing.md#install-from-pypi). +- [Install a KDB-X license](../getting-started/installing.md#install-a-kdb-x-license). +- If you are new to KDB-X Python, complete the [quickstart guide](../getting-started/quickstart.md) first. +- Run the examples with [Python 3](../getting-started/installing.md#prerequisites) in an interactive session, a `.py` script, or a Jupyter notebook. + +Run each code sample in order because later examples use objects created earlier. + +## 1. Import KDB-X Python + +Import the `pykx` package with the conventional `kx` alias: + +```python +>>> import pykx as kx +``` + +The remaining examples also use NumPy and pandas: + +```python +>>> import numpy as np +>>> import pandas as pd +``` + +Set the random seed so generated data remains reproducible throughout the guide: + +```python +>>> kx.random.seed(42) +``` + +## 2. Data structures + +KDB-X Python wraps values of [q data types](https://code.kx.com/q/basics/datatypes/) in Python classes from the `pykx` package. Pass these objects directly to q functions and KDB-X queries. + +This section introduces the structures used throughout the guide: + +- 2.1 [Atom](#21-atom) +- 2.2 [Vector](#22-vector) +- 2.3 [List](#23-list) +- 2.4 [Dictionary](#24-dictionary) +- 2.5 [Table](#25-table) +- 2.6 [Function types](#26-function-types) + +### 2.1 Atom + +An **atom** contains one value of a specific q type. The following examples create `pykx.FloatAtom` and `pykx.DateAtom` objects from equivalent Python values: + +```python +>>> float_atom = kx.FloatAtom(1.0) +>>> print(type(float_atom).__name__, float_atom.py()) +``` + +```text +FloatAtom 1.0 +``` + +```python +>>> from datetime import date +>>> +>>> date_atom = kx.DateAtom(date(2020, 1, 1)) +>>> print(type(date_atom).__name__, date_atom.py()) +``` + +```text +DateAtom 2020-01-01 +``` + +### 2.2 Vector + +A `pykx` vector contains multiple values of one type. Vectors form the columns in dictionaries and tables and support efficient vector operations. + +Vectors are one-dimensional and support indexing along one axis. The following examples convert NumPy and pandas objects to typed vectors: + +```python +>>> print(kx.IntVector(np.array([1, 2, 3, 4], dtype=np.int32))) +``` + +```text +1 2 3 4i +``` + +```python +>>> print(kx.toq(pd.Series([1, 2, 3, 4]))) +``` + +```text +1 2 3 4 +``` + +### 2.3 List + +A `pykx.List` can contain values of different types. Use lists for mixed or nested data that cannot use a single typed vector. + +Lists can also hold multidimensional and ragged data. Use a typed vector when every item has the same q type. + +```python +>>> print(kx.List([[1, 2, 3], [1.0, 1.1, 1.2], ['a', 'b', 'c']])) +``` + +```text +1 2 3 +1 1.1 1.2 +a b c +``` + +### 2.4 Dictionary + +A `pykx.Dictionary` maps keys directly to values. The key list and value list must contain the same number of items. + +```python +>>> print(kx.Dictionary({'x': [1, 2, 3], 'x1': np.array([1, 2, 3])})) +``` + +```text +x | 1 2 3 +x1| 1 2 3 +``` + +### 2.5 Table + +A `pykx.Table` stores named, typed columns in memory. Each column has a q type, and table methods operate on the columns. + +KDB-X Python provides several table types: + +- `pykx.Table` +- `pykx.KeyedTable` +- `pykx.SplayedTable` +- `pykx.PartitionedTable` + +This section introduces the two in-memory table types. + +#### `pykx.Table` + +The Quickstart guide creates a table from Python column data. The following example creates a table from rows: + +```python +>>> print( +... kx.Table( +... [[1, 2, 'a'], [2, 3, 'b'], [3, 4, 'c']], +... columns=['col1', 'col2', 'col3'], +... ) +... ) +``` + +```text +col1 col2 col3 +-------------- +1 2 a +2 3 b +3 4 c +``` + +#### `pykx.KeyedTable` + +A `pykx.KeyedTable` assigns one or more columns as keys. It behaves similarly to a pandas `DataFrame` with an index. Call `set_index()` on a table to create one. + +[Read the `pykx.KeyedTable` API reference](../api/pykx-q-data/wrappers.md#pykx.wrappers.KeyedTable). + +```python +>>> print( +... kx.Table( +... data={ +... 'x': [1, 2, 3], +... 'x1': [2, 3, 4], +... 'x2': ['a', 'b', 'c'], +... } +... ).set_index(['x']) +... ) +``` + +```text +x| x1 x2 +-| ----- +1| 2 a +2| 3 b +3| 4 c +``` + +### 2.6 Function types + +KDB-X Python wraps q functions so you can call them from Python. + +#### `pykx.Lambda` + +A `pykx.Lambda` represents a q function with up to eight parameters. You can call it with KDB-X Python or compatible Python values. + +```python +>>> pykx_lambda = kx.q('{x+y}') +>>> print(type(pykx_lambda)) +``` + +```text + +``` + +```python +>>> print(pykx_lambda(1, 2)) +``` + +```text +3 +``` + +#### `pykx.Projection` + +A projection fixes some arguments of a function in advance, similar to [`functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial). Call the projection with only the remaining arguments. + +If a function accepts `n` parameters and you provide `m`, the resulting projection accepts `n - m` parameters. + +```python +>>> projection = kx.q('{x+y}')(1) +>>> print(projection) +``` + +```text +{x+y}[1] +``` + +```python +>>> print(projection(2)) +``` + +```text +3 +``` + +## 3. Create and access `pykx` objects + +This section applies the data structures from the previous section to common tasks: + +- 3.1 [Create objects from Python data](#31-create-objects-from-python-data) +- 3.2 [Generate random data](#32-generate-random-data) +- 3.3 [Generate data with q](#33-generate-data-with-q) +- 3.4 [Read a CSV file](#34-read-a-csv-file) +- 3.5 [Optional: query a q process over IPC](#35-optional-query-a-q-process-over-ipc) + +### 3.1 Create objects from Python data + +KDB-X Python converts data between typed `pykx` objects and common Python formats. + +Call `kx.toq()` to convert supported native Python, NumPy, pandas, and PyArrow data to a typed `pykx` object: + +```python +>>> pydict = {'a': [1, 2, 3], 'b': ['a', 'b', 'c'], 'c': 2} +>>> print(kx.toq(pydict)) +``` + +```text +a| 1 2 3 +b| `a`b`c +c| 2 +``` + +```python +>>> nparray = np.array([1, 2, 3, 4], dtype=np.int32) +>>> q_vector = kx.toq(nparray) +>>> print(q_vector) +``` + +```text +1 2 3 4i +``` + +```python +>>> pdframe = pd.DataFrame(data={'a': [1, 2, 3], 'b': ['a', 'b', 'c']}) +>>> print(kx.toq(pdframe)) +``` + +```text +a b +--- +1 a +2 b +3 c +``` + +To convert a `pykx` object back to a Python-compatible representation, use: + +| **Method** | **Converts to** | +| --- | --- | +| `.py()` | Native Python values | +| `.np()` | NumPy | +| `.pd()` | pandas | +| `.pa()` | PyArrow | + +For example: + +```python +>>> print(q_vector.py()) +>>> print(repr(q_vector.np())) +``` + +```text +[1, 2, 3, 4] +array([1, 2, 3, 4], dtype=int32) +``` + +PyArrow is an optional dependency. For more examples and conversion considerations, read [Create and convert KDB-X Python objects](../user-guide/fundamentals/creating.md). + +### 3.2 Generate random data + +Use `kx.random.random()` to generate typed test data. + +The following example generates 10,000 random floating-point values between 0 and 1, then confirms the number of values: + +```python +>>> random_values = kx.random.random(10_000, 1.0) +>>> print(len(random_values)) +``` + +```text +10000 +``` + +Pass a list as the second argument to choose randomly from its values: + +```python +>>> print(kx.random.random(5, [1, ['a', 'b', 'c'], np.array([1.1, 1.2, 1.3])])) +``` + +```text +1 +1.1 1.2 1.3 +1.1 1.2 1.3 +1 +`a`b`c +``` + +Pass a list of dimensions as the first argument to create multidimensional data. Typed null and infinity values let you generate data across the range supported by that q type: + +```python +>>> print(kx.random.random([2, 5], kx.GUIDAtom.null)) +``` + +```text +cddd3245-b888-e3e6-6b46-238afd6899da 6710ad06-0667-4588-61da-11b89be0d75b 6ac.. +88c4ce9d-bebb-8cef-5ad5-7059b07b1136 99016def-c977-34a2-4065-634ee6441b32 af8.. +``` + +```python +>>> print(kx.random.random([2, 3, 4], kx.IntAtom.inf)) +``` + +```text +1297399104 2021175493 1561022346 494454701 1253362152 1017691991 185388196 .. +1508381055 1409761995 429286974 1445067192 1912108436 1867348694 2131953451 .. +``` + +Set a seed globally or for one call when you need reproducible data: + +```python +>>> kx.random.seed(10) +>>> print(kx.random.random(10, 2.0)) +``` + +```text +0.1782082 1.669039 0.7243899 1.999868 0.7675971 1.723838 0.1836728 0.5061767 .. +``` + +```python +>>> print(kx.random.random(10, 2.0, seed=10)) +``` + +```text +0.1782082 1.669039 0.7243899 1.999868 0.7675971 1.723838 0.1836728 0.5061767 .. +``` + +### 3.3 Generate data with q + +KDB-X Python lets you execute q directly from Python with `kx.q`. + +Create a q vector: + +```python +>>> print(kx.q('0 1 2 3 4')) +``` + +```text +0 1 2 3 4 +``` + +```python +>>> print(kx.q('([idx:desc til 5]col1:til 5;col2:5?1f;col3:5?`2)')) +``` + +```text +idx| col1 col2 col3 +---| -------------------- +4 | 0 0.8619188 ol +3 | 1 0.09183638 mg +2 | 2 0.2530883 cm +1 | 3 0.2504566 cc +0 | 4 0.7517286 jg +``` + +Pass arguments to a q function: + +```python +>>> print(kx.q('{x+y}', 1, 2)) +``` + +```text +3 +``` + +### 3.4 Read a CSV file + +KDB-X Python provides `kx.q.read.csv()` for reading CSV data into typed q columns. The following example creates a small CSV file: + +```python +>>> import csv +>>> +>>> with open('pykx.csv', 'w', newline='', encoding='utf-8') as csv_file: +... writer = csv.writer(csv_file) +... fields = ["name", "age", "height", "country"] +... +... writer.writerow(fields) +... writer.writerow(["Oladele Damilola", "40", "180.0", "Nigeria"]) +... writer.writerow(["Alina Hricko", "23", "179.2", "Ukraine"]) +... writer.writerow(["Isabel Walter", "50", "179.5", "United Kingdom"]) +... +``` + +```python +>>> print( +... kx.q.read.csv( +... 'pykx.csv', +... types={'age': kx.LongAtom, 'country': kx.SymbolAtom}, +... ) +... ) +``` + +```text +name age height country +-------------------------------------------- +"Oladele Damilola" 40 180 Nigeria +"Alina Hricko" 23 179.2 Ukraine +"Isabel Walter" 50 179.5 United Kingdom +``` + +```python +>>> import os +>>> os.remove('pykx.csv') +``` + +### 3.5 Optional: query a q process over IPC + +Complete this optional section when you need to query a separate q process. It requires the `q` executable on your system path. [Install KDB-X](https://code.kx.com/kdb-x/get_started/kdb-x-install.html) before you continue. + +!!! note "If you run this example on Windows" + + Use Windows Subsystem for Linux (WSL) with KDB-X installed, or connect to an existing q process instead. + +The example starts a local q process on port 5000, connects synchronously, and queries it with q, the KDB-X Python query API, and SQL. Skip this section if your environment does not allow subprocesses or local network connections. + +```python +>>> import subprocess +>>> import time +>>> +>>> try: +... with kx.PyKXReimport(): +... proc = subprocess.Popen(('q', '-p', '5000')) +... except OSError as error: +... raise kx.QError('Unable to create a q process on port 5000') from error +... +>>> conn = None +>>> try: +... time.sleep(2) +... conn = kx.SyncQConnection(port=5000) +... +... conn('system "S 42"') +... conn('tab:([]col1:100?`a`b`c;col2:100?1f;col3:100?0Ng)') +... print('q query:') +... print(conn('select from tab where col1=`a')[:5]) +... +... print('\nQuery API:') +... print( +... conn.qsql.select( +... 'tab', +... where=(kx.Column('col1') == 'a') & (kx.Column('col2') > 0.3), +... )[:5] +... ) +... +... print('\nSQL:') +... print(conn.sql('SELECT * FROM tab where col2>=0.5')[:5]) +... finally: +... if conn is not None: +... conn.close() +... if proc.poll() is None: +... proc.terminate() +... proc.wait(timeout=5) +... +``` + +```text +q query: +col1 col2 col3 +--------------------------------------------------- +a 0.6357471 22371003-8997-eed1-f4df-58fcdedd8376 +a 0.1200245 1a43967f-d414-1b0e-ae8c-0c411890e175 +a 0.8362442 23befa0d-8324-099e-296f-93e9d7fc70d6 +a 0.8223493 2fe5ff57-2d7f-f1d9-024b-7c258e1fcd9a +a 0.5082821 fac43ae3-caf2-1c2a-3951-aa857f3a7b8a + +Query API: +col1 col2 col3 +--------------------------------------------------- +a 0.6357471 22371003-8997-eed1-f4df-58fcdedd8376 +a 0.8362442 23befa0d-8324-099e-296f-93e9d7fc70d6 +a 0.8223493 2fe5ff57-2d7f-f1d9-024b-7c258e1fcd9a +a 0.5082821 fac43ae3-caf2-1c2a-3951-aa857f3a7b8a +a 0.9455612 418c580d-e1c4-093f-d532-38f2a35686fc + +SQL: +col1 col2 col3 +--------------------------------------------------- +c 0.9805637 84cf32c6-c711-79b4-2f31-6e85923decff +a 0.6357471 22371003-8997-eed1-f4df-58fcdedd8376 +a 0.8362442 23befa0d-8324-099e-296f-93e9d7fc70d6 +b 0.9044767 3796c7db-e028-e16a-422f-43e2dadbc5b9 +c 0.965964 82261eb4-b3a4-67cb-8374-d9c22c68be6d +``` + +The `finally` block closes the connection and stops the q process if a query raises an error. + +--- + +## 4. Analyze vectors and tables + +KDB-X Python provides Python methods and q functions for vector and table calculations. + +- 4.1 [Use built-in vector methods](#41-use-built-in-vector-methods) +- 4.2 [Use built-in table methods](#42-use-built-in-table-methods) +- 4.3 [Use q functions](#43-use-q-functions) + +### 4.1 Use built-in vector methods + +Call methods such as `mean()` and `max()` directly on a typed vector: + +```python +>>> q_vector = kx.random.random(1000, 10.0) +``` + +```python +>>> print(q_vector.mean()) +``` + +```text +4.984157 +``` + +```python +>>> print(q_vector.max()) +``` + +```text +9.998212 +``` + +Use `apply()` when you need a custom Python calculation: + +```python +>>> def bespoke_function(x, y): +... return x*y +... +>>> print(q_vector.apply(bespoke_function, 5)) +``` + +```text +31.74132 38.3376 46.40922 10.17963 38.73944 48.33864 41.12562 45.44382 32.290.. +``` + +### 4.2 Use built-in table methods + +KDB-X Python tables provide methods for filtering, grouping, and calculating column values. The [pandas API notebook](../user-guide/advanced/Pandas_API.ipynb) documents the table methods that mirror pandas operations. + +The following examples use a typed table with symbol and numeric columns: + +```python +>>> N = 10_000 +>>> example_table = kx.Table( +... data={ +... 'sym': kx.random.random(N, ['a', 'b', 'c']), +... 'col1': kx.random.random(N, 10.0), +... 'col2': kx.random.random(N, 20), +... } +... ) +>>> print(example_table[:5]) +``` + +```text +sym col1 col2 +----------------- +b 5.332563 3 +c 1.555556 10 +c 7.583763 18 +b 9.657423 12 +b 4.543002 17 +``` + +Select multiple columns by passing their names as a list: + +```python +>>> print(example_table[['sym', 'col1']]) +``` + +```text +sym col1 +------------- +b 5.332563 +c 1.555556 +c 7.583763 +b 9.657423 +b 4.543002 +b 5.587764 +c 1.137493 +a 1.328115 +c 4.453789 +c 0.8913583 +b 1.720334 +a 7.381833 +a 6.678592 +c 0.4029847 +b 1.705896 +a 1.209391 +a 6.059422 +c 1.296014 +a 2.615679 +b 9.125557 +.. +``` + +Filter table rows with `loc`, as you would with a pandas `DataFrame`: + +```python +>>> print(example_table.loc[example_table['sym'] == 'a']) +``` + +```text +sym col1 col2 +------------------- +a 1.328115 2 +a 7.381833 2 +a 6.678592 6 +a 1.209391 16 +a 6.059422 15 +a 2.615679 0 +a 2.779952 17 +a 3.281611 18 +a 6.036149 9 +a 0.5906303 11 +a 0.06815037 0 +a 8.130013 7 +a 2.149836 3 +a 4.742463 5 +a 5.857722 4 +a 5.858365 13 +a 8.713641 18 +a 5.651717 18 +a 2.719246 3 +a 6.195479 15 +.. +``` + +Apply the same Boolean filter with `[]`, which calls `__getitem__()`: + +```python +>>> print(example_table[example_table['sym'] == 'b']) +``` + +```text +sym col1 col2 +----------------- +b 5.332563 3 +b 9.657423 12 +b 4.543002 17 +b 5.587764 4 +b 1.720334 8 +b 1.705896 10 +b 9.125557 9 +b 5.04443 5 +b 8.741315 10 +b 7.250155 3 +b 2.437429 15 +b 7.900336 12 +b 9.095591 4 +b 5.145199 11 +b 7.107319 17 +b 8.117879 19 +b 5.078329 9 +b 8.56087 3 +b 9.312045 15 +b 8.32909 0 +.. +``` + +Call `set_index()` to convert a `pykx.Table` to a `pykx.KeyedTable`: + +```python +>>> print(example_table.set_index('sym')) +``` + +```text +sym| col1 col2 +---| -------------- +b | 5.332563 3 +c | 1.555556 10 +c | 7.583763 18 +b | 9.657423 12 +b | 4.543002 17 +b | 5.587764 4 +c | 1.137493 13 +a | 1.328115 2 +c | 4.453789 4 +c | 0.8913583 13 +b | 1.720334 8 +a | 7.381833 2 +a | 6.678592 6 +c | 0.4029847 12 +b | 1.705896 10 +a | 1.209391 16 +a | 6.059422 15 +c | 1.296014 5 +a | 2.615679 0 +b | 9.125557 9 +.. +``` + +Apply table analytics such as `mean()` and `median()`: + +```python +>>> print('mean:') +>>> print(example_table.mean(numeric_only=True)) +>>> +>>> print('median:') +>>> print(example_table.median(numeric_only=True)) +``` + +```text +mean: +col1| 4.941699 +col2| 9.5393 +median: +col1| 4.927338 +col2| 10 +``` + +Group the table by `sym`, then calculate the mean of each numeric column: + +```python +>>> print(example_table.groupby('sym').mean()) +``` + +```text +sym| col1 col2 +---| ----------------- +a | 4.906693 9.568162 +b | 5.053115 9.494075 +c | 4.867251 9.554758 +``` + +Apply a custom function that converts each group to NumPy before calculating a result: + +```python +>>> def apply_func(x): +... nparray = x.np() +... return np.sqrt(nparray).mean() +... +>>> +>>> print(example_table.groupby('sym').apply(apply_func)) +``` + +```text +sym| col1 col2 +---| ----------------- +a | 2.085255 2.867129 +b | 2.12296 2.863945 +c | 2.071932 2.865273 +``` + +Use `merge_asof()` to join time-series tables. This example creates a trades table and a quotes table with temporal columns: + +```python +>>> trades = kx.Table( +... data={ +... "time": [ +... pd.Timestamp("2016-05-25 13:30:00.023"), +... pd.Timestamp("2016-05-25 13:30:00.023"), +... pd.Timestamp("2016-05-25 13:30:00.030"), +... pd.Timestamp("2016-05-25 13:30:00.041"), +... pd.Timestamp("2016-05-25 13:30:00.048"), +... pd.Timestamp("2016-05-25 13:30:00.049"), +... pd.Timestamp("2016-05-25 13:30:00.072"), +... pd.Timestamp("2016-05-25 13:30:00.075"), +... ], +... "ticker": [ +... "GOOG", +... "MSFT", +... "MSFT", +... "MSFT", +... "GOOG", +... "AAPL", +... "GOOG", +... "MSFT", +... ], +... "bid": [720.50, 51.95, 51.97, 51.99, 720.50, 97.99, 720.50, 52.01], +... "ask": [720.93, 51.96, 51.98, 52.00, 720.93, 98.01, 720.88, 52.03], +... } +... ) +>>> quotes = kx.Table( +... data={ +... "time": [ +... pd.Timestamp("2016-05-25 13:30:00.023"), +... pd.Timestamp("2016-05-25 13:30:00.038"), +... pd.Timestamp("2016-05-25 13:30:00.048"), +... pd.Timestamp("2016-05-25 13:30:00.048"), +... pd.Timestamp("2016-05-25 13:30:00.048"), +... ], +... "ticker": ["MSFT", "MSFT", "GOOG", "GOOG", "AAPL"], +... "price": [51.95, 51.95, 720.77, 720.92, 98.0], +... "quantity": [75, 155, 100, 100, 100], +... } +... ) +>>> +>>> print('trades:') +>>> print(trades) +>>> print('\nquotes:') +>>> print(quotes) +``` + +```text +trades: +time ticker bid ask +------------------------------------------------- +2016.05.25D13:30:00.023000000 GOOG 720.5 720.93 +2016.05.25D13:30:00.023000000 MSFT 51.95 51.96 +2016.05.25D13:30:00.030000000 MSFT 51.97 51.98 +2016.05.25D13:30:00.041000000 MSFT 51.99 52 +2016.05.25D13:30:00.048000000 GOOG 720.5 720.93 +2016.05.25D13:30:00.049000000 AAPL 97.99 98.01 +2016.05.25D13:30:00.072000000 GOOG 720.5 720.88 +2016.05.25D13:30:00.075000000 MSFT 52.01 52.03 + +quotes: +time ticker price quantity +---------------------------------------------------- +2016.05.25D13:30:00.023000000 MSFT 51.95 75 +2016.05.25D13:30:00.038000000 MSFT 51.95 155 +2016.05.25D13:30:00.048000000 GOOG 720.77 100 +2016.05.25D13:30:00.048000000 GOOG 720.92 100 +2016.05.25D13:30:00.048000000 AAPL 98 100 +``` + +Apply an as-of join by `ticker` so each trade matches the most recent quote for the same ticker: + +```python +>>> print(trades.merge_asof(quotes, on='time', by='ticker')) +``` + +```text +time ticker bid ask price quantity +----------------------------------------------------------------- +2016.05.25D13:30:00.023000000 GOOG 720.5 720.93 +2016.05.25D13:30:00.023000000 MSFT 51.95 51.96 51.95 75 +2016.05.25D13:30:00.030000000 MSFT 51.97 51.98 51.95 75 +2016.05.25D13:30:00.041000000 MSFT 51.99 52 51.95 155 +2016.05.25D13:30:00.048000000 GOOG 720.5 720.93 720.92 100 +2016.05.25D13:30:00.049000000 AAPL 97.99 98.01 98 100 +2016.05.25D13:30:00.072000000 GOOG 720.5 720.88 720.92 100 +2016.05.25D13:30:00.075000000 MSFT 52.01 52.03 51.95 155 +``` + +### 4.3 Use q functions + +Call q functions through `kx.q` when they provide the operation you need. The following examples introduce mathematical, iteration, and table functions: + +- 4.3.1 [Mathematical functions](#431-mathematical-functions) +- 4.3.2 [Iteration functions](#432-iteration-functions) +- 4.3.3 [Table functions](#433-table-functions) + +#### 4.3.1 Mathematical functions + +##### `mavg` + +Calculate moving averages with a rolling window: + +```python +>>> moving_average = kx.q.mavg(10, kx.random.random(10000, 2.0)) +>>> print(moving_average[:10]) +``` + +```text +1.909677 1.326592 1.390974 1.228086 1.006547 0.847965 0.9194783 0.836154 0.86.. +``` + +##### `cor` + +Calculate the correlation between two lists: + +```python +>>> print(kx.q.cor([1, 2, 3], [2, 3, 4])) +``` + +```text +1f +``` + +```python +>>> print(kx.q.cor(kx.random.random(100, 1.0), kx.random.random(100, 1.0))) +``` + +```text +0.009771912 +``` + +##### `prds` + +Calculate cumulative products for a list: + +```python +>>> print(kx.q.prds([1, 2, 3, 4, 5])) +``` + +```text +1 2 6 24 120 +``` + +#### 4.3.2 Iteration functions + +##### `each` + +Use `each` as a q function or a `pykx.Lambda` method to apply a function to every item: + +```python +>>> print(kx.q.each(kx.q('{prd x}'), kx.random.random([5, 5], 10.0, seed=10))) +``` + +```text +1033.597 377.1784 7126.713 418.3232 89.97531 +``` + +```python +>>> print(kx.q('{prd x}').each(kx.random.random([5, 5], 10.0, seed=10))) +``` + +```text +1033.597 377.1784 7126.713 418.3232 89.97531 +``` + +#### 4.3.3 Table functions + +##### `meta` + +Return metadata for a table: + +```python +>>> qtab = kx.Table( +... data={ +... 'x': kx.random.random(1000, ['a', 'b', 'c']).grouped(), +... 'y': kx.random.random(1000, 1.0), +... 'z': kx.random.random(1000, kx.TimestampAtom.inf), +... } +... ) +``` + +```python +>>> print(kx.q.meta(qtab)) +``` + +```text +c| t f a +-| ----- +x| s g +y| f +z| p +``` + +##### `xasc` + +Sort a table in ascending order by one or more columns: + +```python +>>> print(kx.q.xasc('z', qtab)[:5]) +``` + +```text +x y z +------------------------------------------ +c 0.2660419 2000.09.17D00:27:33.222932480 +b 0.2378591 2001.02.01D19:58:48.496586752 +c 0.05802967 2001.05.29D15:29:16.181340160 +c 0.9474748 2003.03.24D08:12:02.975653888 +b 0.02726729 2004.01.31D07:25:21.959215104 +``` + +## Next steps + +Continue with [Objects and attributes](../learn/objects.md) to understand the type information available on KDB-X Python objects. + +Or explore by task: + +- Explore [data creation and conversion](../user-guide/fundamentals/creating.md). +- Learn how to [index and slice `pykx` objects](../user-guide/fundamentals/indexing.md). +- Use [NumPy functions with `pykx` vectors](../user-guide/advanced/numpy.md). +- Choose a [query interface](../user-guide/fundamentals/query/index.md). +- Learn how to [communicate over IPC](../user-guide/advanced/ipc.md). +- Refer to the [q functions and operators reference](../api/pykx-execution/q.md). +- [Troubleshoot errors](../help/troubleshooting.md). diff --git a/docs/examples/subscriber/readme.md b/docs/examples/subscriber/readme.md index da68756..cf925e5 100644 --- a/docs/examples/subscriber/readme.md +++ b/docs/examples/subscriber/readme.md @@ -18,7 +18,9 @@ The following python libraries are required to run this example: 1. KDB-X Python (`pykx`) 1. asyncio -The source code for this example is available in the examples directory here: +To follow along, [download and extract the example files](archive.zip). The archive contains the synchronous and asynchronous subscriber scripts. + +You can also view the scripts in the source repository: 1. [Synchronous subscriber](https://github.com/KxSystems/pykx/blob/main/examples/subscriber/subscriber.py) 1. [Asynchronous subscriber](https://github.com/KxSystems/pykx/blob/main/examples/subscriber/subscriber_async.py) @@ -197,5 +199,5 @@ This example has demonstrated how to initiate a q process, subscribe to an exist Check out more examples such as: -- [Real-Time Streaming] -- [Compression and Encryption] +- [Real-time streaming](../streaming/index.md) +- [Compress and encrypt](../compress_and_encrypt/readme.md) diff --git a/docs/examples/threaded_execution/archive.zip b/docs/examples/threaded_execution/archive.zip index 6e58739..c3d18e9 100644 Binary files a/docs/examples/threaded_execution/archive.zip and b/docs/examples/threaded_execution/archive.zip differ diff --git a/docs/examples/threaded_execution/threading.md b/docs/examples/threaded_execution/threading.md index e1a1216..8422cf6 100644 --- a/docs/examples/threaded_execution/threading.md +++ b/docs/examples/threaded_execution/threading.md @@ -79,6 +79,10 @@ background thread must be shutdown when finished. The easiest way to ensure this a `#!python try` - `#!python finally` block around the entrypoint to your script. This ensures that even in the event of an error, the background thread shuts down correctly so Python can exit. +When using the Python `threading` library you must ensure that you call `pykx.core.m9()` at the end of the threads +life to ensure memory is not leaked. This is not required in the asyncio example as asyncio will reuse the thread +again for other tasks. Calling it when the thread will be reused will result in undefined behaviour. + ``` import os os.environ['PYKX_THREADING'] = '1' diff --git a/docs/examples/threaded_execution/threads.py b/docs/examples/threaded_execution/threads.py index f93689e..f04cef9 100644 --- a/docs/examples/threaded_execution/threads.py +++ b/docs/examples/threaded_execution/threads.py @@ -25,6 +25,7 @@ def upsert_threaded(q, calls): counter -= 1 if counter <= 0: break + kx.core.m9() return 0 diff --git a/docs/extras/glossary.md b/docs/extras/glossary.md index 9a857eb..42d4866 100644 --- a/docs/extras/glossary.md +++ b/docs/extras/glossary.md @@ -86,4 +86,4 @@ The `#!python unique` attribute ensures that all items in the `#!python Vector`/ In the context of databases, upsert is an operation that combines both updating and inserting data into a table. When you perform an upsert, the database checks whether a record with a specific key already exists in the table. If a record with that key exists, the database updates the existing record with new values. If no record with that key exists, the database inserts a new record with the provided data. Learn more about [upsert](../api/pykx-execution/q.md#upsert). ## Vector -A vector is a mathematical concept used to represent quantities that have both magnitude (size) and direction. In other words, vectors are arrays of numerical values that represent points in multidimensional space. A vector is typically represented as an arrow in space, pointing from one point to another. Learn more about [using in-built methods on KDB-X Python vectors](../examples/interface-overview.ipynb#using-in-built-methods-on-pykx-vectors) and [adding values to KDB-X Python vectors/lists](../user-guide/fundamentals/indexing.md#b-assigning-and-adding-values-to-vectorslists). +A vector is a mathematical concept used to represent quantities that have both magnitude (size) and direction. In other words, vectors are arrays of numerical values that represent points in multidimensional space. A vector is typically represented as an arrow in space, pointing from one point to another. Learn more about [using in-built methods on KDB-X Python vectors](../examples/interface-overview.md#41-use-built-in-vector-methods) and [adding values to KDB-X Python vectors/lists](../user-guide/fundamentals/indexing.md#b-assigning-and-adding-values-to-vectorslists). diff --git a/docs/getting-started/installing.md b/docs/getting-started/installing.md index d0a23a8..63dad4d 100644 --- a/docs/getting-started/installing.md +++ b/docs/getting-started/installing.md @@ -1,402 +1,239 @@ --- -title: KDB-X Python installation guide -description: Getting started with KDB-X Python -last_updated: October 2025 +title: Install KDB-X Python +description: Install KDB-X Python, configure a KDB-X license, verify the installation, and prepare air-gapped environments +last_updated: July 2026 author: KX Systems, Inc., -tags: KDB-X Python, setup, install, +keywords: + - KDB-X Python + - pykx + - install + - pip + - license + - air-gapped environments + - Windows --- -# KDB-X Python installation guide -_This page explains how to install KDB-X Python on your machine._ +# Install KDB-X Python -!!! License +_Install KDB-X Python, configure a license, and verify the installation._ - KDB-X Python is released under a dual license covering the files within the [KDB-X Python repository](https://github.com/kxsystems/pykx) as outlined [here](../license.md). +**Estimated time:** 5–10 minutes for a standard `pip` installation, license setup, and verification. + +!!! warning "Software license terms" + + KX releases KDB-X Python under a dual license that covers the files in the [KDB-X Python repository](https://github.com/kxsystems/pykx). Review the [KDB-X Python license terms](../license.md) before installation. **Acceptance of license terms:** - By downloading, installing, or using KDB-X Python, you acknowledge and agree that you have read, understood, and accept the license [link](../license.md) and will adhere to its terms. + By downloading, installing, or using KDB-X Python, you acknowledge and agree that you have read, understood, and accept the [KDB-X Python license terms](../license.md). -## Pre-requisites +## Prerequisites Before you start, make sure you have: -- [**Python**](https://www.python.org/downloads/) (versions 3.9-3.14) +- [**Python**](https://www.python.org/downloads/) 3.9-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. - -## Supported environments - -KX only supports versions of KDB-X Python built by KX (installed from wheel files) for: - -- **Linux** (`manylinux2014_x86_64`, `manylinux2014_aarch64`) with CPython 3.9-3.14 -- **macOS** (`macosx_10_15_x86_64`, `macosx_10_15_arm64`) with CPython 3.9-3.14 -- **Windows** (`win_amd64`) with CPython 3.9-3.14 - -We provide assistance to user-built installations of KDB-X Python only on a best-effort basis. - -## 1. Install KDB-X Python - -You can install KDB-X Python from three sources: - -!!! Note "Installing in air-gapped environments" - - If you are installing in a location without internet connection you may find [this section](#installing-in-an-air-gapped-environment) useful. - -=== "Install KDB-X Python from PyPI" - - Ensure you have a recent version of `#!bash pip`: +Create and activate a virtual environment to isolate KDB-X Python from other projects: - ```sh - pip install --upgrade pip - - ``` - Then install the latest version of KDB-X Python with the following command: +=== "macOS and Linux" ```sh - pip install --upgrade pykx - + python3 -m venv .venv + source .venv/bin/activate ``` -=== "Install KDB-X Python from Anaconda" - - For Linux x86 and arm-based architectures, you can install KDB-X Python from the `#!bash kx` channel on Anaconda as follows: - - ```sh - conda install -c kx KDB-X Python +=== "Windows PowerShell" + ```powershell + py -m venv .venv + .\.venv\Scripts\Activate.ps1 ``` - Type `#!bash y` when prompted to accept the installation. + ??? tip "If PowerShell blocks virtual environment activation" -=== "Install KDB-X Python from GitHub" - - Clone the `pykx` repository: - - ```sh - git clone https://github.com/kxsystems/pykx - - ``` - - Enter the cloned repository and install KDB-X Python using `#!bash pip`: + PowerShell may require permission to run the activation script. Enable locally created scripts for the current PowerShell session, then activate the environment: - ```sh - cd pykx - pip install . - - ``` - -=== "Install KDB-X Python 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 KDB-X Python with the following command: - - ```sh - uv pip install pykx - - ``` - -=== "Install KDB-X Python from pipx" - - Ensure you have `#!bash pipx` installed, installation guide can be found [here.](https://pipx.pypa.io/stable/installation/) - - Then install KDB-X Python using the flag to install dependencies: - - ```sh - pipx install pykx --include-deps - - ``` - - This will create a virtual environment to run KDB-X Python in, make sure you activate this to access the package. + ```powershell + Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned + .\.venv\Scripts\Activate.ps1 + ``` + When prompted, confirm the policy change. The setting applies only to the current PowerShell session. PowerShell discards it when you close the window. -At this point you have [partial access to KDB-X Python](../user-guide/advanced/modes.md#1a-running-in-unlicensed-mode). To gain access to all KDB-X Python features, follow the steps in the next section, otherwise go straight to [3. Verify KDB-X Python Installation](#3-verify-pykx-installation). + If an organizational policy prevents this change, run subsequent commands with `.\.venv\Scripts\python.exe` instead of `python`. +## Supported environments -## 2. Install a KDB-X license +KX supports KDB-X Python wheels for CPython 3.9-3.14 on Linux, macOS, and Windows. -To use all KDB-X Python functionalities, you need to download and install a KDB-X license. -To obtain a license, go to the [KX Developer Center](https://developer.kx.com/products/kdb-x/install). +??? info "Supported wheel platform tags" -### 2.a Install an existing license in Python + The wheel platform tag depends on the operating system and, for Linux, the CPython version: -Follow the steps below to install a license for KDB-X Python: + | **Platform** | **CPython versions** | **Wheel platform tags** | + | --- | --- | --- | + | Linux | 3.9-3.11 | `manylinux2014_x86_64`, `manylinux2014_aarch64` | + | Linux | 3.12-3.14 | `manylinux_2_28_x86_64`, `manylinux_2_28_aarch64` | + | macOS | 3.9-3.14 | `macosx_10_15_x86_64`, `macosx_10_15_arm64` | + | Windows | 3.9-3.14 | `win_amd64` | -1. Start your Python session: +## Install from PyPI - ```bash - $ python - ``` +PyPI is the recommended installation method for most users. If you manage packages with Conda, uv, or pipx, or need to build from source, expand **Other installation methods**. For an offline system, follow [Air-gapped installation](#air-gapped-installation). -2. Import the KDB-X Python library. When prompted to accept the installation, type `#!python Y` or press `#!python Enter`: +!!! info "Why the package is named `pykx`" - ```python - >>> import pykx as kx + KDB-X Python is the product name, but the Python distribution and import package remain `pykx` for compatibility with applications written for PyKX 3.x. Use `pykx` in package-manager commands and `import pykx` in Python code. - Thank you for installing KDB-X Python! + If you are upgrading from PyKX 3.x, review the [migration guide](../upgrades/3040.md). - We have been unable to locate your license for KDB-X Python. Running KDB-X Python in unlicensed mode has reduced functionality. - Would you like to install a license? [Y/n]: - ``` +Upgrade `pip`: -3. Indicate whether you have access to an existing KDB-X Python enabled license or not, type `#!python Y` if you have a license: +```sh +python -m pip install --upgrade pip +``` - ```python - Do you have access to an existing license for KDB-X Python that you would like to use? [N/y]: - ``` +Install the latest KDB-X Python release from PyPI: -4. Choose the desired method to activate your license by typing `#!python 1` or `#!python 2` as appropriate: +```sh +python -m pip install --upgrade pykx +``` - ```bash - Please select the method you wish to use to activate your license: - [1] Provide the location of your license file - [2] Paste the license key - ``` +??? info "Other installation methods" -5. Depending on your choice (`#!python 1` or `#!python 2`), complete the installation by following the final steps as below: + === "Conda" - === "1" + On Linux x86 and ARM architectures, create an environment and install KDB-X Python from the `kx` channel: - ```bash - Provide the download location of your license (for example, ~/path/to/kc.lic): + ```sh + conda create --name kdbx-python python + conda activate kdbx-python + conda install -c kx pykx ``` - === "2" + === "uv" - Specify the license type: + Create a virtual environment and install KDB-X Python: - ```bash - Please confirm the license type: - [1] kc.lic - The default - [2] k4.lic - Used in some scenarios + ```sh + uv venv + uv pip install pykx ``` - Then paste your license key and hit enter: - - ```bash - Provide your kc.lic license key (base64 encoded string) : - ``` + === "pipx" -6. Validate the correct installation of your license: + Install KDB-X Python and its dependencies in an isolated virtual environment: - ```python - >>> kx.q.til(10) - pykx.LongVector(pykx.q('0 1 2 3 4 5 6 7 8 9')) - ``` + ```sh + pipx install pykx --include-deps + ``` -### 2.b Install license with environment variables + Activate the environment that pipx creates before you import KDB-X Python. -For environment-specific flexibility, there are two ways to install your license: by using a file or by copying text. Click on the tabs below, read the instructions, and choose the method you wish to follow: + === "Build from source" -!!! Note "" + Install KDB-X Python directly from the repository: - === "Using a file" + ```sh + git clone https://github.com/kxsystems/pykx + cd pykx + python -m pip install . + ``` - 1. Download your license file. + Building from source requires Git and may require platform-specific build tools. KX provides best-effort support for user-built installations. - 2. Set an environment variable pointing to the folder with the license file. (Learn how to set environment variables from [here](https://chlee.co/how-to-setup-environment-variables-for-windows-mac-and-linux/)). - * **Variable Name**: `#!bash QLIC` - * **Variable Value**: `#!bash /user/path/to/folder` +Without a KDB-X license, KDB-X Python runs with reduced functionality. To continue without a license, skip to [Verify the installation](#verify-the-installation). - === "Using text" +## Install a KDB-X license - 1. Copy the `#!bash base64` encoded contents of your license key provided. +Before you start, make sure you have one of these KDB-X licenses ready: - 2. On your computer, set an environment variable with the contents of your `kc.lic` license key named `#!bash KDB_LICENSE_B64`. Or use `KDB_K4LICENSE_B64` if you have an older `k4.lic` license key. (Learn how to set environment variables from [here](https://chlee.co/how-to-setup-environment-variables-for-windows-mac-and-linux/)). - * **Variable Name**: `KDB_LICENSE_B64` / `KDB_K4LICENSE_B64` - * **Variable Value**: `` +- A `kc.lic` license key from the [KX Developer Center](https://developer.kx.com/products/kdb-x/install). +- A `k4.lic` license provided separately by KX. -To validate if you successfully installed your license with environment variables, start Python and import KDB-X Python as follows: +[Review the KDB-X license requirements](https://code.kx.com/kdb-x/get_started/kdb-x-install.html#license-requirements) for more information. -```bash -$ python ->>> import pykx as kx ->>> kx.q.til(5) -pykx.LongVector(pykx.q('0 1 2 3 4')) -``` +=== "Python prompt" -!!! Tip "Tip: automatic license renewal setup" + Start an interactive Python session and import KDB-X Python: - When your license nears its expiry date, you can set KDB-X Python to automatically renew it. To do this, modify the environment variable `#!bash KDB_LICENSE_B64` or `#!bash KDB_K4LICENSE_B64` with your new license information. When KDB-X Python initializes with the expired license, it will attempt to overwrite it with the new value: + ```sh + $ python + ``` - ```shell - $python + ```python >>> import pykx as kx - Initialisation failed with error: exp - Your license has been updated using the following information: - Environment variable: 'KDB_K4LICENSE_B64' - License write location: /user/path/to/license/k4.lic ``` -## 3. Verify KDB-X Python installation - -To verify if you successfully installed KDB-X Python on your system, run: - -```bash -python -c"import pykx;print(pykx.__version__)" -``` - -This command should display the installed version of KDB-X Python. - -## Dependencies - -??? Info "Expand for Required and Optional KDB-X Python dependencies" - - === "Required" - - KDB-X Python depends on the following third-party Python packages: - - - `pandas>=1.2; 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` - - `requests>=2.25.0` + When prompted, confirm that you want to install a license. Choose a license file or base64-encoded license key, then provide its location or value. - **Note**: All are installed automatically by `#!bash pip` when you install KDB-X Python. +=== "Environment variables" - Here's a breakdown of how KDB-X Python uses these libraries: + Configure one license source before starting Python: - - [NumPy](https://pypi.org/project/numpy): converts data from `pykx` objects to NumPy equivalent Array/Recarray style objects; direct calls to NumPy functions such as `numpy.max` with `pykx` objects relies on the NumPy Python API. - - [Pandas](https://pypi.org/project/pandas): converts KDB-X Python data to Pandas Series/DataFrame equivalent objects or to PyArrow data formats. Pandas is used as an intermediary data format. - - [pytz](https://pypi.org/project/pytz/): converts data with timezone information to `pykx` objects to ensure that the offsets are accurately applied. - - [toml](https://pypi.org/project/toml/): for configuration parsing and management, with `config-KDB-X Python` as outlined [here](../user-guide/configuration.md). - - [dill](https://pypi.org/project/dill): used in the serialization and deserialization of Python objects when interfacing between q and Python processes using [remote functions](../user-guide/advanced/remote-functions.md) or [real-time capture](../user-guide/advanced/streaming/index.md) functionality. + | **License source** | **Environment variable** | **Value** | + | --- | --- | --- | + | `kc.lic` or `k4.lic` file | `QLIC` | Directory that contains the license file | + | Base64-encoded `kc.lic` key | `KDB_LICENSE_B64` | License key supplied by KX | + | Base64-encoded `k4.lic` key | `KDB_K4LICENSE_B64` | License key supplied by KX | + KDB-X Python reads the configured license when the Python process starts. - === "Optional" +## Verify the installation - **Optional Python dependencies:** +Print the installed version and active license mode: - - **`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]` - - **`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]` - - Here's a breakdown of how KDB-X Python uses these libraries: - - - [PyArrow](https://pypi.org/project/pyarrow): converts `pykx` objects to and from their PyArrow equivalent table/array objects. - - [find-libpython](https://pypi.org/project/find-libpython): provides the `libpython.{so|dll|dylib}` file required by [KDB-X Python under q](../pykx-under-q/intro.md). - - [ast2json](https://pypi.org/project/ast2json/): required for KX Dashboards Direct integration. - - [psutil](https://pypi.org/project/psutil/): facilitates the stopping and killing of a q process on a specified port allowing for orphaned q processes to be stopped, functionality defined [here](../api/util.md#pykxutilkill_q_process). - - [torch](https://pytorch.org/docs/stable/): required for conversions between `#!python torch.Tensor` objects and their KDB-X Python equivalents. - - **Optional non-Python dependencies:** - - - `libssl` for TLS on [IPC connections](../api/ipc.md). - - `libpthread` on Linux/MacOS when using the `PYKX_THREADING` environment variable. - -!!! Note "Troubleshooting and Support" - - If you encounter any issues during the installation process, refer to the following sources for assistance: - - - Visit our [troubleshooting](../help/troubleshooting.md) guide. - - Ask a question on the KX community at [learninghub.kx.com](https://learninghub.kx.com/forums/forum/pykx/). - - Use Stack Overflow and tag [`pykx`](https://stackoverflow.com/questions/tagged/pykx) or [`kdb`](https://stackoverflow.com/questions/tagged/kdb) depending on the subject. - - Go to [support](../help/support.md). - -## Asset Information - -| Platform | Mode | File | Version | -| --------- | ------------- | ----------- | ------------ | -| Linux ARM | KDB-X | libq.so | 5.0.20260501 | -| Linux x86 | KDB-X | libq.so | 5.0.20260501 | -| Mac ARM | KDB-X | libq.dylib | 5.0.20260501 | -| Mac x86 | KDB-X | libq.dylib | 5.0.20260501 | -| Windows | KDB-X | q.dll/q.lib | 5.0.20260501 | -| Linux ARM | Unlicensed | libe.so | 2023.11.22 | -| Linux x86 | Unlicensed | libe.so | 2023.11.22 | -| Mac ARM | Unlicensed | libe.so | 2023.11.22 | -| Mac x86 | Unlicensed | libe.so | 2023.11.22 | -| Windows | Unlicensed | e.dll/e.lib | 2024.08.21 | - -## Optional: Installing a q executable - -The following section is optional and primarily required if you are looking to make use of the [Real-Time Capture](../user-guide/advanced/streaming/index.md) functionality provided by KDB-X Python. - -### Do I need a q executable? - -For the majority of functionality provided by KDB-X Python you do not explicitly need access to a q executable. Users within a Python process who do not have a q executable will be able to complete tasks such as the following: - -- Convert data to/from Python types -- Run analytics on in-memory and on-disk databases -- Create databases -- Query remote q processes via IPC -- Execute numpy functions with KDB-X Python data - -If however you need to make use of the [Real-Time Capture](../user-guide/advanced/streaming/index.md) functionality you will need access to a q executable. Fundamentally the capture and persistence of real-time data and the application of analytics on this streaming data is supported via deployment of code on q processes. - -### Configuring KDB-X Python to use an existing executable +```sh +python -c "import pykx as kx; print(kx.__version__); print(f'Licensed: {kx.licensed}')" +``` -By default, when initializing a q process within Real-Time Capture workflows, KDB-X Python attempts to call `q` directly. However, this approach can be unreliable when the Python `subprocess` module is used to launch the process. To ensure consistency, configure the environment to explicitly reference the full path to your q executable. +The command prints the KDB-X Python version and one of these results: -If you already have a q executable, KDB-X Python can use this when initializing the Real-Time Capture APIs through the setting of the following in your [configuration file](../user-guide/configuration.md#configuration-file) or as [environment variables](../user-guide/configuration.md#environment-variables): +- `Licensed: True` confirms that KDB-X Python initialized embedded q with the installed license. +- `Licensed: False` confirms that KDB-X Python runs in unlicensed mode. -| **Variable** | **Explanation** | -| :------------------ | :--------------------------------------------------------------------------------------------------------------- | -| `PYKX_Q_EXECUTABLE` | Specifies the location of the q executable which should be called. Typically this will be `QHOME/[lmw]64/q[.exe]`| -| `QHOME` | The directory to which q was installed | +If the result does not match your configuration, refer to [Troubleshooting](../help/troubleshooting.md). -### Installing an executable +## Air-gapped installation -Go to [KDB-X](https://kdb-x.kx.com/) and follow the instructions. +Use a connected system that matches the operating system, architecture, and Python version of the air-gapped system. -### Installing in an air-gapped environment +Prepare a wheelhouse using one of these sources: -Installing Python libraries in air-gapped environments requires users to first download the [Python wheel](https://realpython.com/python-wheels/) files for the libraries you need to install. +=== "PyPI" -!!! Note "Build using the same environment as you're installing" + Download KDB-X Python and its dependencies: - When downloading the `.whl` files and dependencies make sure you are using the same OS and Python version as you will be when installing in your isolated environment. + ```sh + $ python -m pip download --destination-directory wheelhouse pykx + ``` -Prepare the installation package from an internet-enabled system in one of the following ways: +=== "Build from source" -1. Download the `.whl` file for the OS, library version and Python version you are intending to use on the air-gapped environment. These files can be sourced from [here](https://pypi.org/project/pykx/#files). -2. Generate the `.whl` file from a git clone of the [KDB-X Python repository](https://github.com/kxsystems/pykx). An example of this is as follows: + Clone the repository and build KDB-X Python and its dependencies: - ```bash + ```sh $ git clone https://github.com/kxsystems/pykx $ cd pykx - $ pip install build - # The below will install the `.whl` to a `dist/` folder - $ python -m build . - ``` -After downloading the `*.whl` file locally, you can retrieve and save its required dependencies using the following command: - - ```bash - $ pip download dist/*.whl + $ python -m pip wheel --wheel-dir wheelhouse . ``` -Copy the content of your `dist/` folder to an external storage device (USB-key etc.) and upload the `.whl` files to your air-gapped device. +Copy the `wheelhouse` directory to the air-gapped system, then install from it. This example uses `/opt/airgap/wheels`: -Install the wheels which for simplicity are stored at a location `/opt/airgap/wheels` - -```bash -pip install --no-cache /opt/airgap/wheels/* +```sh +python -m pip install --no-index --find-links=/opt/airgap/wheels pykx ``` -### Verify KDB-X Python can use the executable +After installation, [install a KDB-X license](#install-a-kdb-x-license) and [verify the installation](#verify-the-installation). -Verifying that KDB-X Python has access to the executable can be done through execution of the function `#!python kx.util.start_q_subprocess` and requires either your configuration file or environment variables to include `PYKX_Q_EXECUTABLE`. This is outlined [here](#configuring-kdb-x-python-to-use-an-existing-executable). +## Related installation topics -```python ->>> import pykx as kx ->>> server = kx.util.start_q_subprocess(5052) ->>> conn = kx.SyncQConnection(port=5052) # Connect to subprocess ->>> conn('1+1') -pykx.LongAtom(pykx.q('2')) ->>> server.kill() -``` +- [Configure KDB-X Python](../user-guide/configuration.md) +- [Review dependencies and bundled assets](../user-guide/configuration.md#dependencies-and-bundled-assets) +- [Manage or renew a license](../user-guide/advanced/license.md) +- [Get installation help](../help/support.md) ## Next steps -That's it! You can now start using KDB-X Python in your Python projects: - -- [Quickstart guide](quickstart.md) -- [Updating/Upgrading your license](../user-guide/advanced/license.md) +- [Run the quickstart](quickstart.md) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 313a6ef..e97c17a 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,467 +1,154 @@ --- -title: Quickstart for KDB-X Python -description: Quickstart guide for setting up KDB-X Python -last_updated: October 2025 +title: Quickstart for KDB-X Python +description: Create, inspect, and query a q table with KDB-X Python, then convert the result to a pandas DataFrame +last_updated: July 2026 author: KX Systems, Inc., -tags: KDB-X Python, quickstart, import KDB-X Python, use pykx objects +keywords: + - KDB-X Python + - pykx + - quickstart + - q + - table + - pandas + - DataFrame --- # Quickstart -_This quickstart guide provides first time users with essential instructions for using the KDB-X Python library._ +_Create a small dataset, analyze it with q from Python, and convert the result to a pandas `DataFrame`._ + +**Estimated time:** 10 minutes ## Prerequisites -Before you start, make sure to: +Before you start: -- [Install the KDB-X Python library](installing.md#1-install-pykx). -- [Have a license](installing.md#2-install-a-license). +- [Install KDB-X Python](installing.md#install-from-pypi). +- [Install a KDB-X license](installing.md#install-a-kdb-x-license). +- Run the examples with [Python 3](installing.md#prerequisites) in an interactive session, a `.py` script, or a Jupyter notebook. ## 1. Import KDB-X Python -To access KDB-X Python, import it within your Python code using the following syntax: +Import the `pykx` package using the conventional `kx` alias: ```python >>> import pykx as kx ``` -!!! Info "The use of the shortened name `#!python kx` is optional and provides a terse convention for interacting with methods and objects from the KDB-X Python library." -Many examples in this guide make use of NumPy so we import this: +!!! info "The `kx` alias" + + Documentation examples use this optional alias to keep KDB-X Python code concise. + +## 2. Create a table + +Create a table of sample trades from native Python lists: + +```python +>>> trades = kx.Table( +... data={ +... 'symbol': ['AAPL', 'MSFT', 'AAPL', 'MSFT'], +... 'price': [189.0, 374.0, 191.0, 376.0], +... 'size': [100, 200, 150, 100], +... } +... ) +>>> +>>> print(trades) +``` + +KDB-X Python creates a typed `pykx.Table` and stores it in q memory: + +```text +symbol price size +----------------- +AAPL 189 100 +MSFT 374 200 +AAPL 191 150 +MSFT 376 100 +``` + +KDB-X Python converts the Python strings, floats, and integers into their corresponding q types. Keeping the data in q memory lets q functions operate on it without another conversion. + +## 3. Inspect with Python indexing + +KDB-X Python tables support familiar Python indexing. Use a slice to select the first two rows: + +```python +>>> print(trades[:2]) +``` + +```text +symbol price size +----------------- +AAPL 189 100 +MSFT 374 200 +``` + +Pass a list of column names to select specific columns: ```python ->>> import numpy as np +>>> print(trades[['symbol', 'price']]) ``` -## 2. Generate `pykx` objects - -You can generate `pykx` objects in three ways. Click on the tabs below to follow the instructions: - -=== "Use KDB-X Python functions" - - Generate `pykx` objects using `#!python pykx` helper functions: - - ```python - >>> kx.random.random([3, 4], 10.0) - pykx.List(pykx.q(' - 4.976492 4.087545 4.49731 0.1392076 - 7.148779 1.946509 0.9059026 6.203014 - 9.326316 2.747066 0.5752516 2.560658 - ')) - - >>> kx.Table(data = {'x': kx.random.random(10, 10.0), 'x1': kx.random.random(10, ['a', 'b', 'c'])}) - pykx.Table(pykx.q(' - x x1 - ------------ - 0.8123546 a - 9.367503 a - 2.782122 c - 2.392341 a - 1.508133 b - ')) - ``` - -=== "From Python data types" - - Generate `pykx` objects from Python, NumPy, Pandas and PyArrow objects by using the `#!python kx.toq` method: - - ```python - >>> pylist = [10, 20, 30] - >>> qlist = kx.toq(pylist) - >>> qlist - pykx.LongVector(pykx.q('10 20 30')) - - >>> nplist = np.arange(0, 10, 2) - >>> qlist = kx.toq(nplist) - >>> qlist - pykx.LongVector(pykx.q('0 2 4 6 8')) - - >>> import pandas as pd - >>> df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]}) - >>> df - col1 col2 - 0 1 3 - 1 2 4 - >>> qtable = kx.toq(df) - pykx.Table(pykx.q(' - col1 col2 - --------- - 1 3 - 2 4 - ')) - - >>> import pyarrow as pa - >>> patab = pa.Table.from_pandas(df) - >>> patab - pyarrow.Table - col1: int64 - col2: int64 - >>> qtable = kx.toq(patab) - >>> qtable - pykx.Table(pykx.q(' - col1 col2 - --------- - 1 3 - 2 4 - ')) - ``` - -=== "Execute q code" - - Generate `pykx` objects using q by calling `#!python kx.q`: - - ```python - >>> kx.q('10 20 30') - pykx.LongVector(pykx.q('10 20 30')) - - >>> kx.q('([]5?1f;5?`4;5?0Ng)') - pykx.Table(pykx.q(' - x x1 x2 - --------------------------------------------------- - 0.439081 ncej 8c6b8b64-6815-6084-0a3e-178401251b68 - 0.5759051 jogn 5ae7962d-49f2-404d-5aec-f7c8abbae288 - 0.5919004 ciha 5a580fb6-656b-5e69-d445-417ebfe71994 - 0.8481567 hkpb ddb87915-b672-2c32-a6cf-296061671e9d - 0.389056 aeaj 580d8c87-e557-0db1-3a19-cb3a44d623b1 - ')) - ``` - -## 3. Interact with `pykx` objects - -You can interact with KDB-X Python objects in a variety of ways, for example: - -- through [indexing using Pythonic syntax](../user-guide/fundamentals/indexing.md) -- passing [`pykx` objects to q/NumPy](../user-guide/fundamentals/creating.md#converting-pykx-objects-to-pythonic-types) functions -- [querying via Python/SQL/qSQL](..//user-guide/fundamentals/query/index.md) syntax -- by [using the q functionality](../user-guide/advanced/context_interface.md) via the context interface. - -Each way is described in more depth under the **How to** section. For now, we recommend a few examples: - -* Create a KDB-X Python list and interact with it using indexing and slices: - - ```python - >>> qarray = kx.random.random(10, 1.0) - >>> qarray - pykx.FloatVector(pykx.q('0.391543 0.08123546 0.9367503 0.2782122 0.2392341 0.1508133 0.1567317 0.9785 ..')) - >>> qarray[1] - pykx.FloatAtom(pykx.q('0.08123546')) - >>> qarray[1:4] - pykx.FloatVector(pykx.q('0.08123546 0.9367503 0.2782122')) - ``` - -* Assign objects to KDB-X Python lists: - - ```python - >>> qarray = kx.random.random(3, 10.0, seed=10) - pykx.FloatVector(pykx.q('0.891041 8.345194 3.621949')) - >>> qarray[1] = 0.1 - >>> qarray - pykx.FloatVector(pykx.q('0.891041 0.1 3.621949')) - ``` - -* Create a KDB-X Python table and manipulate using Pythonic syntax: - - ```python - >>> N = 100 - >>> qtable = kx.Table( - data={ - 'x': kx.random.random(N, 1.0), - 'x1': 5 * kx.random.random(N, 1.0), - 'x2': kx.random.random(N, ['a', 'b', 'c']) - } - ) - >>> qtable - pykx.Table(pykx.q(' - x x1 x2 - ----------------------- - 0.3550381 1.185644 c - 0.3615143 2.835405 a - 0.9089531 2.134588 b - 0.2062569 3.852387 a - 0.481821 0.07970141 a - 0.2065625 1.786519 a - 0.5229178 0.1273692 c - 0.3338806 3.440445 c - 0.414621 3.188777 c - 0.9725813 0.1922818 b - 0.5422726 4.486179 b - 0.6116582 3.967756 a - 0.3414991 1.018642 b - 0.9516746 3.878809 c - 0.1169475 0.3469163 c - 0.8158957 2.050957 a - 0.6091539 1.168774 a - 0.9830794 3.562923 b - 0.7543122 0.6961287 a - 0.3813679 1.350938 b - .. - ')) - >>> qtable[['x', 'x1']] - pykx.List(pykx.q(' - 0.3550381 0.3615143 0.9089531 0.2062569 0.481821 0.2065625 0.5229178 0.3338.. - 1.185644 2.835405 2.134588 3.852387 0.07970141 1.786519 0.1273692 3.4404.. - ')) - >>> qtable[0:5] - pykx.Table(pykx.q(' - x x1 x2 - ----------------------- - 0.3550381 1.185644 c - 0.3615143 2.835405 a - 0.9089531 2.134588 b - 0.2062569 3.852387 a - 0.481821 0.07970141 a - ')) - ``` - -* Pass a KDB-X Python object to a q function: - - ```python - >>> qfunction = kx.q('{x+til 10}') - >>> 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..')) - ``` - -* Apply a Python function on a KDB-X Python vector: - - ```python - >>> qvec = kx.random.random(10, 10, seed=42) - >>> qvec - pykx.LongVector(pykx.q('4 7 2 2 9 4 2 0 8 0')) - >>> qvec.apply(lambda x:x+1) - pykx.LongVector(pykx.q('5 8 3 3 10 5 3 1 9 1')) - ``` - -* Pass `pykx` arrays of objects to NumPy functions: - - ```python - >>> qarray1 = kx.random.random(10, 1.0) - >>> qarray1 - pykx.FloatVector(pykx.q('0.7880561 0.9677446 0.9325539 0.6501981 0.4837422 0.5338642 0.5156039 0.31358..')) - >>> qarray2 = kx.random.random(10, 1.0) - >>> qarray2 - pykx.FloatVector(pykx.q('0.04164985 0.6417901 0.1608836 0.691249 0.4832847 0.6339534 0.4614883 0.06373..')) - - >>> np.max(qarray1) - 0.9677445779088885 - >>> np.sum(kx.random.random(10, 10)) - 43 - >>> np.add(qarray1, qarray2) - pykx.FloatVector(pykx.q('0.8297059 1.609535 1.093438 1.341447 0.9670269 1.167818 0.9770923 0.3773123 1..')) - ``` - -* Query using SQL/qSQL: - - ```python - >>> N = 100 - >>> qtable = kx.Table( - data={ - 'x': kx.random.random(N, ['a', 'b', 'c']), - 'x1': kx.random.random(N, 1.0), - 'x2': 5 * kx.random.random(N, 1.0), - } - ) - >>> qtable[0:5] - pykx.Table(pykx.q(' - x x1 x2 - ---------------------- - a 0.8236115 0.7306473 - a 0.3865843 1.01605 - c 0.9931491 1.155324 - c 0.9362009 1.569154 - c 0.4849499 0.09870703 - ')) - >>> kx.q.sql("SELECT * FROM $1 WHERE x='a'", qtable) - pykx.Table(pykx.q(' - x x1 x2 - --------------------- - a 0.8236115 0.7306473 - a 0.3865843 1.01605 - a 0.259265 2.805719 - a 0.6140826 1.730398 - a 0.6212161 3.97236 - .. - ')) - >>> qtable.select(where=kx.Column('x')=='a') - pykx.Table(pykx.q(' - x x1 x2 - --------------------- - a 0.8236115 0.7306473 - a 0.3865843 1.01605 - a 0.259265 2.805719 - a 0.6140826 1.730398 - a 0.6212161 3.97236 - .. - ')) - ``` - -* Apply q keyword functions: - - ```python - >>> qvec = kx.q.til(10) - >>> qvec - pykx.LongVector(pykx.q('0 1 2 3 4 5 6 7 8 9')) - >>> kx.q.mavg(3, qvec) - pykx.FloatVector(pykx.q('0 0.5 1 2 3 4 5 6 7 8')) - ``` - -* Getting help on a q keyword functions (see [the installation docs](../getting-started/installing.md#dependencies)): - - ```Python - >>> help(kx.q.max) - Help on UnaryPrimitive in pykx: - - pykx.UnaryPrimitive = pykx.UnaryPrimitive(pykx.q('max')) - • max - - Maximum. - - >>> pykx.q.max([0, 7, 2, 4 , 1, 3]) - pykx.LongAtom(q('7')) - - - >>> help(kx.q.abs) - Help on UnaryPrimitive in pykx: - - pykx.UnaryPrimitive = pykx.UnaryPrimitive(pykx.q('abs')) - • abs - - Where x is a numeric or temporal, returns the absolute value of x. Null is returned if x is null. - - >>> pykx.q.abs(-5) - pykx.LongAtom(q('5')) - ``` - -## 4. Convert `pykx` objects to Python types - -To convert the objects generated via the KDB-X Python 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" - - ```python - >>> qdictionary = kx.toq({'a': 5, 'b': range(10), 'c': np.random.uniform(low=0.0, high=1.0, size=(5,))}) - >>> qdictionary - pykx.Dictionary(pykx.q(' - a| 5 - b| 0 1 2 3 4 5 6 7 8 9 - c| 0.01450907 0.9131434 0.5745007 0.961908 0.7609489 - ')) - >>> 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" - - ```python - >>> qvec = kx.q('10?5') - >>> qvec.np() - array([0, 2, 4, 1, 2, 1, 0, 1, 0, 1]) - >>> qtab = kx.Table([[np.random.random(), np.random.randint(0, 5)] for _ in range(5)]) - >>> qtab - pykx.Table(pykx.q(' - x x1 - ------------ - 0.8247812 4 - 0.2149847 0 - 0.1007832 2 - 0.4520411 4 - 0.0196153 0 - ')) - >>> qtab.np() - rec.array([(0.82478116, 4), (0.21498466, 0), (0.10078323, 2), - (0.45204113, 4), (0.0196153 , 0)], - dtype=[('x', '>> qvec = kx.toq(np.random.randint(5, size=10)) - >>> qvec.pd() - 0 0 - 1 2 - 2 4 - 3 1 - 4 2 - 5 1 - 6 0 - 7 1 - 8 0 - 9 1 - dtype: int64 - >>> 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 - 0 0.824781 4 - 1 0.214985 0 - 2 0.100783 2 - 3 0.452041 4 - 4 0.019615 0 - ``` - - If using `#!python pandas>=2.0` it is possible to also use the `#!python as_arrow` keyword argument to convert to - pandas types using pyarrow as the backend instead of the default NumPy-backed Pandas objects. - - ```python - >>> qvec = kx.toq(np.random.randint(5, size=10)) - >>> qvec.pd(as_arrow=True) - 0 1 - 1 2 - 2 3 - 3 4 - 4 2 - 5 3 - 6 0 - 7 0 - 8 2 - 9 0 - dtype: int64[pyarrow] - >>> 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 - 0 0.541059 3 - 1 0.886690 1 - 2 0.674300 4 - 3 0.532791 3 - 4 0.523147 4 - >>> qtab.pd(as_arrow=True).dtypes - x double[pyarrow] - x1 int64[pyarrow] - dtype: object - ``` - -=== "Convert to PyArrow" - - ```python - >>> qvec = kx.random.random(10, 5) - >>> qvec.pa() - - [ - 0, - 2, - 4, - 1, - 2, - 1, - 0, - 1, - 0, - 1 - ] - >>> 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 - x: double - x1: int64 - ---- - x: [[0.707331785506831,0.03695847895120696,0.7024166621644556,0.3955776423810857,0.7539328513313873]] - x1: [[4,3,2,3,2]] - ``` +```text +symbol price +------------ +AAPL 189 +MSFT 374 +AAPL 191 +MSFT 376 +``` + +## 4. Analyze with q + +Use `kx.q` to calculate the average price and total size for each symbol: + +```python +>>> summary = kx.q( +... '{select average_price:avg price, total_size:sum size by symbol from x}', +... trades, +... ) +>>> +>>> print(summary) +``` + +The q function receives `trades` as its `x` argument and returns a keyed table: + +```text +symbol| average_price total_size +------| ------------------------ +AAPL | 190 250 +MSFT | 375 300 +``` + +The embedded q runtime executes this query in the same Python process. To query a separate q process, use the [IPC interface](../user-guide/advanced/ipc.md). + +## 5. Convert to a pandas `DataFrame` + +KDB-X Python results remain typed q objects until you choose to convert them. Call `.pd()` when a downstream Python workflow needs a pandas `DataFrame`: + +```python +>>> summary_df = summary.pd() +>>> print(summary_df) +``` + +The resulting `DataFrame` contains one row per symbol: + +```text + average_price total_size +symbol +AAPL 190.0 250 +MSFT 375.0 300 +``` ## Next steps -- [Introduction Notebook](../examples/interface-overview.ipynb#ipc-communication) +Continue with [KDB-X Python Fundamentals](../examples/interface-overview.md) for a guided tour of q types, conversions, table methods, q functions, and IPC. + +Or go directly to a focused guide: + +- [Understand objects and attributes](../learn/objects.md) +- [Create and convert KDB-X Python objects](../user-guide/fundamentals/creating.md) +- [Index KDB-X Python objects](../user-guide/fundamentals/indexing.md) +- [Query data](../user-guide/fundamentals/query/index.md) +- [Communicate over IPC](../user-guide/advanced/ipc.md) +- [Troubleshoot errors](../help/troubleshooting.md) diff --git a/docs/getting-started/what-is-kdb-x-python.md b/docs/getting-started/what-is-kdb-x-python.md new file mode 100644 index 0000000..f1d7a56 --- /dev/null +++ b/docs/getting-started/what-is-kdb-x-python.md @@ -0,0 +1,104 @@ +--- +title: About KDB-X Python +description: Understand KDB-X Python, its audiences, use cases, architecture, and alternatives for Python and q integration +last_updated: July 2026 +author: KX Systems, Inc., +keywords: + - KDB-X Python + - pykx + - Python + - q + - use cases + - architecture + - PyKX migration +--- + +# About KDB-X Python + +_This page describes KDB-X Python, its use cases, and its role in the KDB-X platform._ + +**KDB-X Python** is the official Python interface to **KDB-X**. Python code can create and query q objects, call q functions in the embedded runtime, and connect to separate q processes over IPC. + +## Why use KDB-X Python + +KDB-X Python brings KDB-X data and q execution into familiar Python workflows: + +- Use q vector operations, temporal data types, and queries from a standard Python process. +- Keep licensed, in-process workloads in q memory instead of serializing data through an IPC-only client. +- Convert data to and from native Python, NumPy, pandas, and PyArrow representations when another Python tool needs it. +- Use one KX-supported interface for embedded q, remote q over IPC, and Python under q. + +## Intended audience + +This page applies to: + +- **Python developers** who use q types, q functions, or KDB-X databases from Python. +- **q developers** who call Python libraries from q or manage q through Python. +- **[Existing PyKX users](../upgrades/3040.md)** migrating to KDB-X Python 4.0 or later. +- **Data engineers and data scientists** who access KDB-X data from Python. + +## Use cases + +KDB-X Python supports these main use cases: + +1. Store, query, manipulate, and use [q objects](../learn/objects.md) within a Python process. +2. Query external q processes over an [interprocess communication (IPC)](../user-guide/advanced/ipc.md) interface. +3. Run Python code in a native q session with [KDB-X Python under q](../pykx-under-q/intro.md). + +## KDB-X Python architecture + +KDB-X Python supports Python-first and q-first applications. The following diagram shows how it integrates Python and q through embedded runtimes and IPC. + +For license requirements and feature availability, [compare the modes of operation](../user-guide/advanced/modes.md). + +![KDB-X Python runs embedded q in a Python process, connects to a separate q process over IPC, or runs Python in a q process.](../images/kdb-x-python-architecture-light.svg){ .only-light .no-bg } +![KDB-X Python runs embedded q in a Python process, connects to a separate q process over IPC, or runs Python in a q process.](../images/kdb-x-python-architecture-dark.svg){ .only-dark .no-bg } + +??? note "Learn more about q and KDB-X" + + KDB-X uses the kdb+ execution engine and the q programming language. + + - **KDB-X** extends the kdb+ execution engine with modules and interfaces for q, Python, SQL, and open data formats. It processes data in memory and on disk and treats temporal data as a first-class type. + + - **q** is a concise and expressive programming language that combines vector operations with functional programming. KDB-X Python exposes q types, functions, and the runtime through Python. + + To learn more about q and KDB-X, use these resources: + + - [An introduction to q and KDB-X](https://code.kx.com/q/learn/tour/) + - [Tutorial videos that introduce q and KDB-X](https://code.kx.com/q/learn/q-for-all/) + +## Python and q interfaces + +Three historical interfaces connect Python with q and KDB-X: + +1. [embedPy](https://code.kx.com/q/ml/embedpy) +2. [PyQ](https://github.com/KxSystems/pyq) +3. [qPython](https://github.com/exxeleron/qPython) + +The following table summarizes the key differences between embedPy, PyQ, qPython, and KDB-X Python: + +| **Feature** | **embedPy** | **PyQ** | **qPython** | **KDB-X Python** | +| ---------------- | --------------------------------- | -------------------------------------- | ------------------------------------------------ | ----------------------------------------------------- | +| Interoperability | Calls Python from q | Runs Python and q in the same process | Connects Python to q over IPC | Runs q in process or connects to q over IPC | +| Execution | Runs from a q session | Requires the PyQ binary or a q startup | Runs processing in q and deserializes in Python | Runs from a Python session with a class-based type system | +| Use case | Adds Python features to q | Operates on the same data in both languages | Connects Python applications to q | Stores and queries q objects from Python or a q session | +| Access modes | Calls Python from q | Requires the PyQ binary | IPC | Embedded q, IPC, and Python under q | +| Data conversion | Supports q, NumPy, and Python | Supports q, NumPy, and pandas | Converts data across the socket | Shares q memory and supports NumPy, pandas, and PyArrow | + +??? note "Compare embedPy, PyQ, qPython, and KDB-X Python" + + Compare how each interface operates: + + - **embedPy** calls Python from q but does not call q from Python. q developers use it for Python features such as machine learning, statistical methods, and plotting. + - **PyQ** integrates Python and q interpreters in the same process but requires a PyQ binary or q startup instead of the standard Python executable. + - **qPython** takes a Python-first approach but works entirely over IPC. It serializes Python objects, sends them to q over a socket, and deserializes returned q objects. These operations can use significant processing and memory resources. + - **KDB-X Python** stores, queries, and manipulates q objects in a Python process and queries external q processes over IPC. It provides Python APIs for q types, q functions, q scripts, and IPC connections. + +!!! tip "Support for embedPy, PyQ, and qPython" + + KX maintains embedPy and PyQ on a best-effort basis under the [Fusion](https://code.kx.com/q/interfaces) initiative. KX does not support qPython, which is in maintenance mode. KDB-X Python is the current KX-supported interface for Python and q integration. + +## Next steps + +- [Install KDB-X Python](installing.md) +- Already installed and licensed? [Run the quickstart](quickstart.md) diff --git a/docs/getting-started/what_is_pykx.md b/docs/getting-started/what_is_pykx.md deleted file mode 100644 index bc0adb4..0000000 --- a/docs/getting-started/what_is_pykx.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: What is KDB-X Python -description: Overview of KDB-X Python -last_updated: October 2025 -author: KX Systems, Inc., -tags: about KDB-X Python, q, use cases, ---- - -# What is KDB-X Python? - -_This page briefly describes KDB-X Python, its use cases, and its connection with q._ - -## Introduction - -**KDB-X Python** is a Python-first interface to **KDB-X** (the world's fastest time-series database) and **q** (KDB-X's underlying vector programming language). KDB-X Python is the result of 10+ years of integrations between two languages: Python and q. Its aim is to help users query and analyze huge amounts of in-memory and on-disk time-series data, significantly faster than other libraries. - -- For **Python developers**, KDB-X Python unlocks the speed and power of kdb+ for data processing and storage from within your Python environment. It enables anyone with Python knowledge to apply analytics against vast amounts of data, both in-memory and on-disk, in a fraction of the time, allowing you to focus on getting the best from your data. - -- For **q developers**, KDB-X Python brings together Python's data science ecosystem and the power of kdb+'s vector and time-series analytics. This makes them available in both q and Python environments. You can use it to run q code within a Python environment or embed Python analytics within your q session. - -## Use cases - -KDB-X Python supports three main use cases, allowing Python data engineers and data scientists to: - -1. Store, query, manipulate and use q objects within a Python process. -2. Query external q processes via an [Inter-Process Communication (IPC)](..//user-guide/advanced/ipc.md) interface. -3. Embed Python functionality within a native q session using its [under q](../pykx-under-q/intro.md) functionality. - -??? Note "Expand to learn more about q/KDB-X" - - Used throughout the financial sector for 25+ years, q and KDB-X (formerly known as kdb+) have been a cornerstone of modern financial markets. This technology provides a storage mechanism for historical market data and performant tooling to analyze this vast streaming, real-time and historical data. - - - **KDB-X** is a high-performance column-oriented database designed to process and store large amounts of data. Commonly accessed data is available in RAM which makes it faster to access than disk stored data. Operating with temporal data types as a first class entity the use of q and it's query language qsql against this database creates a highly performant time-series analysis tool available. - - - **q** is the vector programming language which is used for all interactions with KDB-X databases, known both for its speed and expressiveness. KDB-X Python exposes q as a domain-specific language (DSL) embedded within Python. The assumption is that q is mainly used for data processing and database management. - - This approach benefits users in two ways: - - - Helps users familiar with q to make the most of its advanced analytics and database management. - - Empowers KDB-X/q users who lack q expertise to get up and running quickly. - - For more information on using q/KDB-X and getting started with see the following links: - - - [An introduction to q/KDB-X](https://code.kx.com/q/learn/tour/) - - [Tutorial videos introducing q/KDB-X](https://code.kx.com/q/learn/q-for-all/) - -## KDB-X Python vs. Python/q interfaces - -There are three historical interfaces which allow interoperability between Python and q/KDB-X: - -1. [Embedpy](https://code.kx.com/q/ml/embedpy) -2. [PyQ](https://github.com/KxSystems/pyq) -3. [qPython](https://github.com/KxSystems/pyq) - -How does KDB-X Python compare to other q interfaces for Python? Here’s a TL;DR comparison table highlighting the key differences between EmbedPy, PyQ, qPython, and KDB-X Python: - -| **Feature** | **EmbedPy** | **PyQ** | **qPython** | **KDB-X Python** | -| ---------------- | ------------------------------------------------ | ------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| Interoperability | Python from q | Python & q in same process | Python-first, over IPC | Python-first, in-process & IPC | -| Execution | Designed for q developers, run in q session | Requires PyQ binary or start from q | Processing completed on q session via IPC, deserialization using Python | Run from Python session, class-based type system | -| Use Case | Leverage Python functionality not available in q | Operate on same data across languages | Common use case, expensive in processing & memory | Store, query, manipulate q objects within Python, via IPC or Python in q session | -| Flexibility | \- | Locked into using PyQ binary | \- | Pythonic interface, context interface for q scripts, q first mode, IPC available | -| Data Conversion | q to/from NumPy/Python only | q to/from Numpy/Pandas only | Data converted directly from socket | Leverages q memory space embedded within Python, supports NumPy, Pandas, PyArrow | - -??? Note "Expand to learn more about EmbedPy, PyQ, qPython, and KDB-X Python" - - To give you a clear understanding of how each interface operates and their suitability for different use cases, here are some additional details: - - - **EmbedPy** allows using Python from q but does not interface with q from Python. It’s mainly for q developers to access Python functionalities like machine learning, statistical methods, and plotting. - - **PyQ** integrates Python and q interpreters in the same process, but it requires executing a special PyQ binary or starting from q, which is not ideal for Python use cases that require a standard Python binary. - - **qPython** takes a Python-first approach but works entirely over IPC (Inter-Process Communication), meaning Python objects sent to q and q objects returned are serialized, sent over a socket, and then deserialized, which can be resource-intensive. - - **KDB-X Python** supports storing, querying, manipulating, and using q objects within a Python process and querying external q processes via IPC. KDB-X Python provides a more Pythonic approach with a class-based hierarchical type system and a context interface for interacting with q scripts in a Pythonic manner. - -!!! tip "EmbedPy, PyQ, qPython: Interface support" - - KX maintains both embedPy and PyQ on a best-efforts basis under the [Fusion](https://code.kx.com/q/interfaces) initiative. qPython is in maintenance mode, not supported by KX. If you're using EmbedPy, PyQ, or qPython, we recommend switching to KDB-X Python to pick up the latest updates from KX. - -## Next steps - -- Follow the [Installation guide](installing.md) -- Get up and running with the KDB-X Python [Quickstart guide](quickstart.md) -- Get to know more functionalities with the [KDB-X Python Introduction Notebook](../examples/interface-overview.ipynb) diff --git a/docs/help/faq.md b/docs/help/faq.md index 4e719df..900f354 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -47,7 +47,6 @@ pykx.LongVector(pykx.q('0 1 2 3 4 5 6 7 8 9')) The following steps are run by `pykx` to find the license when you execute `#!python import pykx`: 1. Search for **kx.lic**, **kc.lic** and **k4.lic** license files in this order within the following locations: - 1. Current working directory 1. Location defined by environment variable `#!bash QLIC` if set 1. Location defined by environment variable `#!bash QHOME` if set 2. If a license is not found KDB-X Python will use the following environment variables (if they are set) to install and make use of a license: diff --git a/docs/help/issues.md b/docs/help/issues.md index 966d957..778a957 100644 --- a/docs/help/issues.md +++ b/docs/help/issues.md @@ -23,6 +23,7 @@ _This page details known issues and functional limitations when using KDB-X Pyth >>> def func(n=2): ... return n ... +>>> kx.q['func'] = func >>> kx.q('func', None) pykx.LongAtom(pykx.q('2')) >>> kx.q('func', kx.q('::')) @@ -135,14 +136,11 @@ q)f[::] /equivalent 7 ``` -## Pandas +#### `DataFrame.equals` behavior on Pandas 2.2 to 2.x -### Known issues -#### Changes in `DataFrame.equals` behavior from Pandas 2.2.0 - -In Pandas 2.2.0, a difference was introduced in how `DataFrame.equals` handles DataFrames with different `_mgr` types. +On Pandas 2.2 up to (but not including) 3.0, a difference exists in how `DataFrame.equals` handles DataFrames with different `_mgr` types. This is resolved on Pandas 3.0 and later, where `.pd()` builds DataFrames with a standard `BlockManager`. -**Example:** +**Example (Pandas 2.2 to 2.x):** ```python >>> import pandas as pd >>> import pykx as kx @@ -150,15 +148,15 @@ In Pandas 2.2.0, a difference was introduced in how `DataFrame.equals` handles D >>> df1 = pd.DataFrame({'cl': ['foo']}) >>> df2 = kx.q('([] cl:enlist `foo)').pd() ->>> df2.equals(df1) +>>> df1.equals(df2) True ->>> df1.equals(df2) # Prior to Pandas 2.2.0, this would also evaluate to True +>>> df2.equals(df1) # Prior to Pandas 2.2.0, this would also evaluate to True False ``` **Cause:** -Pandas now checks the type of the `_mgr` (dataframes manager) property. KDB-X Python uses a custom `_mgr` implementation for performance optimization. +Pandas 2.2 onwards checks the type of the `_mgr` (dataframe manager) property. On Pandas 2.x, KDB-X Python uses a custom `_mgr` implementation for performance optimization. ```python >>> type(df1._mgr) @@ -168,10 +166,12 @@ Pandas now checks the type of the `_mgr` (dataframes manager) property. KDB-X Py ``` -**Workaround:** +**Workaround (Pandas 2.2 to 2.x):** Comparing the full contents of DataFrames irrespective of `_mgr` types works regardless of order of df1 and df2 in the comparison. To do so, use one of the following approaches: ```python >>> assert (df2 == df1).all().all() # Element-wise comparison >>> pd.testing.assert_frame_equal(df2, df1) # Pandas' built-in test >>> assert df1.compare(df2).empty # Check if there are no differences ``` + +On Pandas < 2.2 or >=3.0 use `.equals` as normal. diff --git a/docs/help/support.md b/docs/help/support.md index a659736..42f3e48 100644 --- a/docs/help/support.md +++ b/docs/help/support.md @@ -13,7 +13,7 @@ _This page provides links users can follow to receive help and support for the K If you have any issues or questions you can post them to the following locations, each of which is monitored by the KDB-X Python development team: -- Ask a question to the KX community at [community.kx.com](https://community.kx.com/t5/PyKX/bd-p/PyKX). +- Ask a question on the [KX Forum](https://forum.kx.com/). - Use Stack Overflow with the tags [pykx](https://stackoverflow.com/questions/tagged/pykx) or [kdb](https://stackoverflow.com/questions/tagged/kdb) depending on the subject. ## Customer Support diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md index 2706054..535fd5e 100644 --- a/docs/help/troubleshooting.md +++ b/docs/help/troubleshooting.md @@ -55,7 +55,7 @@ It usually indicates that your license was not correctly written to disk or a li >>> import pykx as kx >>> kx.license.check('/path/to/incorrect/license.txt') Supplied license information does not match. - Please consider reinstalling your license using pykx.util.install_license + Please consider reinstalling your license using pykx.license.install On disk license: b'Atc/wy/gMjZgIdn1KlT3JVWfVmPk55dtb0YJVes5V4ed9Zxt9UVr8G/A1Q3aWiQEkfjGbwvlJU3GXpUergObvzxGN1iyYGZasG5s8vevfAI2ttndt//Y2thrryoQRm9Dy+DIIcmSufwomL+PMJkZacYc9DM6ipnQsL0KvLwLXLrQC1fBLV2pZHCdYC/nX/KM6uslgip4EoTxZTcx1pQPyTx56QKD4K4JBNimO929w/0+v4Hy2x+DIS3n89vpGmtVvjjFRQtsF6Sjnd+6RnFGk13hRL/DlqHTv2XbZgVv++YOCIc7G55KL6PVJYpB66lq9OiZCEdq2GFJLCn2TNWGJPT2s1YDAKsAPI5W3PqJkC2UeV17gPG4gxlCSHr0kfacINbEJ0kSTm/UsuEBZ5B/jvR/jU7rFErcd9PECeQA1kXB19fa4hgvbd+SxWTPxMUKbiHThHk6X0Bi3T7WAQ+sZWsEWwkMncd+mOGS3D+bRav2nfOpKckj8rCdvYum3U8PDv6IHP=S+LaCnJM0yqNjW9xGyog5mlbX2k3mBRyBjbJH/1OWTcIg7uDYxxoMtDOCJjeBdSqI=aK+5FVTVarfowvudv7QsMGeohGaJMyczNWVPPjsbyvsxbAwdXvJUuP0jcFCFVeF' @@ -67,31 +67,31 @@ It usually indicates that your license was not correctly written to disk or a li === "Encoded string based checking" - The following shows a successful check being completed: + The following example checks a valid encoded license string: - ```python - >>> import pykx as kx - >>> license_string = 'Atc/wy/gMjZgIdn1KlT3JVWfVmPk55dtb0YJVes5V4ed9Zxt9UVr8G/A1Q3aWiQEkfjGbwvlJU3GXpUergObvzxGN1iyYGZasG5s8vevfAI2ttndt//Y2thrryoQRm9Dy+DIIcmSufwomL+PMJkZacYc9DM6ipnQsL0KvLwLXLrQC1fBLV2pZHCdYC/nX/KM6uslgip4EoTxZTcx1pQPyTx56QKD4K4JBNimO929w/0+v4Hy2x+DIS3n89vpGmtVvjjFRQtsF6Sjnd+6RnFGk13hRL/DlqHTv2XbZgVv++YOCIc7G55KL6PVJYpB66lq9OiZCEdq2GFJLCn2TNWGJPT2s1YDAKsAPI5W3PqJkC2UeV17gPG4gxlCSHr0kfacINbEJ0kSTm/UsuEBZ5B/jvR/jU7rFErcd9PECeQA1kXB19fa4hgvbd+SxWTPxMUKbiHThHk6X0Bi3T7WAQ+sZWsEWwkMncd+mOGS3D+bRav2nfOpKckj8rCdvYum3U8PDv6IHP=S+LaCnJM0yqNjW9xGyog5mlbX2k3mBRyBjbJH/1OWTcIg7uDYxxoMtDOCJjeBdSqI=aK+5FVTVarfowvudv7QsMGeohGaJMyczNWVPPjsbyvsxbAwdXvJUuP0jcFCFVeF' - >>> kx.license.check(license_string, format = 'STRING') - True - ``` + ```python + >>> import pykx as kx + >>> license_string = 'Atc/wy/gMjZgIdn1KlT3JVWfVmPk55dtb0YJVes5V4ed9Zxt9UVr8G/A1Q3aWiQEkfjGbwvlJU3GXpUergObvzxGN1iyYGZasG5s8vevfAI2ttndt//Y2thrryoQRm9Dy+DIIcmSufwomL+PMJkZacYc9DM6ipnQsL0KvLwLXLrQC1fBLV2pZHCdYC/nX/KM6uslgip4EoTxZTcx1pQPyTx56QKD4K4JBNimO929w/0+v4Hy2x+DIS3n89vpGmtVvjjFRQtsF6Sjnd+6RnFGk13hRL/DlqHTv2XbZgVv++YOCIc7G55KL6PVJYpB66lq9OiZCEdq2GFJLCn2TNWGJPT2s1YDAKsAPI5W3PqJkC2UeV17gPG4gxlCSHr0kfacINbEJ0kSTm/UsuEBZ5B/jvR/jU7rFErcd9PECeQA1kXB19fa4hgvbd+SxWTPxMUKbiHThHk6X0Bi3T7WAQ+sZWsEWwkMncd+mOGS3D+bRav2nfOpKckj8rCdvYum3U8PDv6IHP=S+LaCnJM0yqNjW9xGyog5mlbX2k3mBRyBjbJH/1OWTcIg7uDYxxoMtDOCJjeBdSqI=aK+5FVTVarfowvudv7QsMGeohGaJMyczNWVPPjsbyvsxbAwdXvJUuP0jcFCFVeF' + >>> kx.license.check(license_string, format='STRING') + True + ``` - The following shows an example of a failed check: + The following example checks an invalid encoded license string: - ```python - >>> import pykx as kx + ```python + >>> import pykx as kx >>> license_string = '8nD+HkcJ93xW4oOEtHIZxeWkA1glv5wJ5wE2Fsmbc4lg2ntT9JpsclE1hFeG/Ox/jM4=6GjXD2VNpiCAJ80DNVcXuDB+IPEnP22DMGvBIolJt2pdy9kooGZNQpr6svIkRWX/0m/SbydbQOQUVvfNTxsDjZvvsCiGkdQtygs3sDEJbxsT+KfjqJ7Sd6RQ/47HJHG4JyIWdhmvEBVGSLBa5mdAaCLWdCrga3hHZbW3F4e/l3K4nOQvU91WEiMd6PT061r66AOYmjGACCXqmQ9kSsJfMTXPRi9M2i93Oyv895kFVKdZCLCdKdaow790RcjwnKjFFOERGcge=lZdRtp2BLA+JbixvTIKTObmfqr7uPYsGQLfXSFnQCq7jbt3yxv1ZPjvjYLPTx7YKIvgo+ITG6vyYe+cfwaW1g0tlvFTcVSVb/sxUvvLCLiWMdxGjt5JUxV3GaSm9ysHVk5MrTDpp/5qqXes1/BOXsD2DmS/QSZr/Mt+Vc2baKuxPw1w5YnGVuY6vHxHffABzkn+WPcguabr86JcmIAcC0zc2TLkbufBPJewYka9PIt1Ng283NKe13huPUohnryYVIMPyjrTWpDid+yC5kSGVeP0/5+rJvLmFZUB/n0RUjgMZU5V++GPU1QnCBa+' - >>> kx.license.check(license_string, format = 'STRING') - Supplied license information does not match. - Please consider reinstalling your license using pykx.util.install_license + >>> kx.license.check(license_string, format='STRING') + Supplied license information does not match. + Please consider reinstalling your license using pykx.license.install - On disk license: - b'Atc/wy/gMjZgIdn1KlT3JVWfVmPk55dtb0YJVes5V4ed9Zxt9UVr8G/A1Q3aWiQEkfjGbwvlJU3GXpUergObvzxGN1iyYGZasG5s8vevfAI2ttndt//Y2thrryoQRm9Dy+DIIcmSufwomL+PMJkZacYc9DM6ipnQsL0KvLwLXLrQC1fBLV2pZHCdYC/nX/KM6uslgip4EoTxZTcx1pQPyTx56QKD4K4JBNimO929w/0+v4Hy2x+DIS3n89vpGmtVvjjFRQtsF6Sjnd+6RnFGk13hRL/DlqHTv2XbZgVv++YOCIc7G55KL6PVJYpB66lq9OiZCEdq2GFJLCn2TNWGJPT2s1YDAKsAPI5W3PqJkC2UeV17gPG4gxlCSHr0kfacINbEJ0kSTm/UsuEBZ5B/jvR/jU7rFErcd9PECeQA1kXB19fa4hgvbd+SxWTPxMUKbiHThHk6X0Bi3T7WAQ+sZWsEWwkMncd+mOGS3D+bRav2nfOpKckj8rCdvYum3U8PDv6IHP=S+LaCnJM0yqNjW9xGyog5mlbX2k3mBRyBjbJH/1OWTcIg7uDYxxoMtDOCJjeBdSqI=aK+5FVTVarfowvudv7QsMGeohGaJMyczNWVPPjsbyvsxbAwdXvJUuP0jcFCFVeF' + On disk license: + b'Atc/wy/gMjZgIdn1KlT3JVWfVmPk55dtb0YJVes5V4ed9Zxt9UVr8G/A1Q3aWiQEkfjGbwvlJU3GXpUergObvzxGN1iyYGZasG5s8vevfAI2ttndt//Y2thrryoQRm9Dy+DIIcmSufwomL+PMJkZacYc9DM6ipnQsL0KvLwLXLrQC1fBLV2pZHCdYC/nX/KM6uslgip4EoTxZTcx1pQPyTx56QKD4K4JBNimO929w/0+v4Hy2x+DIS3n89vpGmtVvjjFRQtsF6Sjnd+6RnFGk13hRL/DlqHTv2XbZgVv++YOCIc7G55KL6PVJYpB66lq9OiZCEdq2GFJLCn2TNWGJPT2s1YDAKsAPI5W3PqJkC2UeV17gPG4gxlCSHr0kfacINbEJ0kSTm/UsuEBZ5B/jvR/jU7rFErcd9PECeQA1kXB19fa4hgvbd+SxWTPxMUKbiHThHk6X0Bi3T7WAQ+sZWsEWwkMncd+mOGS3D+bRav2nfOpKckj8rCdvYum3U8PDv6IHP=S+LaCnJM0yqNjW9xGyog5mlbX2k3mBRyBjbJH/1OWTcIg7uDYxxoMtDOCJjeBdSqI=aK+5FVTVarfowvudv7QsMGeohGaJMyczNWVPPjsbyvsxbAwdXvJUuP0jcFCFVeF' - Supplied string content: - b'8nD+HkcJ93xW4oOEtHIZxeWkA1glv5wJ5wE2Fsmbc4lg2ntT9JpsclE1hFeG/Ox/jM4=6GjXD2VNpiCAJ80DNVcXuDB+IPEnP22DMGvBIolJt2pdy9kooGZNQpr6svIkRWX/0m/SbydbQOQUVvfNTxsDjZvvsCiGkdQtygs3sDEJbxsT+KfjqJ7Sd6RQ/47HJHG4JyIWdhmvEBVGSLBa5mdAaCLWdCrga3hHZbW3F4e/l3K4nOQvU91WEiMd6PT061r66AOYmjGACCXqmQ9kSsJfMTXPRi9M2i93Oyv895kFVKdZCLCdKdaow790RcjwnKjFFOERGcge=lZdRtp2BLA+JbixvTIKTObmfqr7uPYsGQLfXSFnQCq7jbt3yxv1ZPjvjYLPTx7YKIvgo+ITG6vyYe+cfwaW1g0tlvFTcVSVb/sxUvvLCLiWMdxGjt5JUxV3GaSm9ysHVk5MrTDpp/5qqXes1/BOXsD2DmS/QSZr/Mt+Vc2baKuxPw1w5YnGVuY6vHxHffABzkn+WPcguabr86JcmIAcC0zc2TLkbufBPJewYka9PIt1Ng283NKe13huPUohnryYVIMPyjrTWpDid+yC5kSGVeP0/5+rJvLmFZUB/n0RUjgMZU5V++GPU1QnCBa+' - False - ``` + Supplied string content: + b'8nD+HkcJ93xW4oOEtHIZxeWkA1glv5wJ5wE2Fsmbc4lg2ntT9JpsclE1hFeG/Ox/jM4=6GjXD2VNpiCAJ80DNVcXuDB+IPEnP22DMGvBIolJt2pdy9kooGZNQpr6svIkRWX/0m/SbydbQOQUVvfNTxsDjZvvsCiGkdQtygs3sDEJbxsT+KfjqJ7Sd6RQ/47HJHG4JyIWdhmvEBVGSLBa5mdAaCLWdCrga3hHZbW3F4e/l3K4nOQvU91WEiMd6PT061r66AOYmjGACCXqmQ9kSsJfMTXPRi9M2i93Oyv895kFVKdZCLCdKdaow790RcjwnKjFFOERGcge=lZdRtp2BLA+JbixvTIKTObmfqr7uPYsGQLfXSFnQCq7jbt3yxv1ZPjvjYLPTx7YKIvgo+ITG6vyYe+cfwaW1g0tlvFTcVSVb/sxUvvLCLiWMdxGjt5JUxV3GaSm9ysHVk5MrTDpp/5qqXes1/BOXsD2DmS/QSZr/Mt+Vc2baKuxPw1w5YnGVuY6vHxHffABzkn+WPcguabr86JcmIAcC0zc2TLkbufBPJewYka9PIt1Ng283NKe13huPUohnryYVIMPyjrTWpDid+yC5kSGVeP0/5+rJvLmFZUB/n0RUjgMZU5V++GPU1QnCBa+' + False + ``` ## Environment issues diff --git a/docs/images/kdb-x-python-architecture-dark.svg b/docs/images/kdb-x-python-architecture-dark.svg new file mode 100644 index 0000000..6bb4c34 --- /dev/null +++ b/docs/images/kdb-x-python-architecture-dark.svg @@ -0,0 +1,121 @@ + + KDB-X Python architecture + Licensed Python applications use KDB-X Python with an embedded q runtime. Licensed and unlicensed Python applications can connect to a separate q process over IPC. Licensed q applications use KDB-X Python under q with an embedded Python runtime. + + + + + + + + + Python process + + + Python + application + + + KDB-X Python + (pykx) + + + Embedded q + runtime + Licensed mode + + + Python API + + + + + Separate q process + + + q server + Data, functions, databases + + + IPC + + + q process + + + q application + or session + + + KDB-X Python + under q + + + Embedded Python + runtime and libraries + Licensed mode + + + q API + + + diff --git a/docs/images/kdb-x-python-architecture-light.svg b/docs/images/kdb-x-python-architecture-light.svg new file mode 100644 index 0000000..63222c5 --- /dev/null +++ b/docs/images/kdb-x-python-architecture-light.svg @@ -0,0 +1,120 @@ + + KDB-X Python architecture + Licensed Python applications use KDB-X Python with an embedded q runtime. Licensed and unlicensed Python applications can connect to a separate q process over IPC. Licensed q applications use KDB-X Python under q with an embedded Python runtime. + + + + + + + + + Python process + + + Python + application + + + KDB-X Python + (pykx) + + + Embedded q + runtime + Licensed mode + + + Python API + + + + + Separate q process + + + q server + Data, functions, databases + + + IPC + + + q process + + + q application + or session + + + KDB-X Python + under q + + + Embedded Python + runtime and libraries + Licensed mode + + + q API + + + diff --git a/docs/img/404/not-found-dark.svg b/docs/img/404/not-found-dark.svg new file mode 100644 index 0000000..6c7b0b1 --- /dev/null +++ b/docs/img/404/not-found-dark.svg @@ -0,0 +1,46 @@ + + Page not found illustration + An interrupted Python code path running through a stylized data grid. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 404 + diff --git a/docs/img/404/not-found-light.svg b/docs/img/404/not-found-light.svg new file mode 100644 index 0000000..37fd78e --- /dev/null +++ b/docs/img/404/not-found-light.svg @@ -0,0 +1,46 @@ + + Page not found illustration + An interrupted Python code path running through a stylized data grid. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 404 + diff --git a/docs/index.md b/docs/index.md index 8dbcc80..ac6c428 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,66 +1,84 @@ -# KDB-X Python Home +--- +title: KDB-X Python documentation +description: Find KDB-X Python installation and migration guidance, Quickstart, learning resources, how-to guides, integrations, examples, and API reference +last_updated: July 2026 +author: KX Systems, Inc., +keywords: + - KDB-X Python + - pykx + - Python + - q + - KDB-X + - documentation +--- -_Welcome to the home of KDB-X Python technical documentation._ +# KDB-X Python -[KDB-X Python](./getting-started/what_is_pykx.md) is a Python-first interface to kdb+, the world's fastest time-series database, and its underlying vector programming language q. Whether you're a Python developer looking to harness kdb+'s analytics, or a q developer wanting to leverage Python's data science ecosystem, KDB-X Python bridges both worlds. +_Documentation for installing KDB-X Python and using q data types, queries, Python conversions, and IPC._ -## Ready to dive in? +[KDB-X Python](./getting-started/what-is-kdb-x-python.md) is the official Python interface to KDB-X. Python code can create and query q objects, call q functions in the embedded runtime, and connect to separate q processes over IPC. + +!!! info "Migration from PyKX" + + **KDB-X Python** (`pykx>=4.0`) is the evolution of **PyKX** (`pykx<4.0`). If you are upgrading, [read the migration guide](upgrades/3040.md) to understand what changed and how to update your code and configuration. + +## Start here + +New users should complete the **Get Started** steps in order. Otherwise, choose the relevant topic area.
- -## What's next? - -Discover how KDB-X Python builds upon KDB-X and what's coming soon: +## Stay informed and get help -- Submit a [feature request](https://forum.kx.com/c/ideas-feature-requests/10) -- Stay updated with the latest [release notes](release-notes/changelog.md){target="_blank"} -- Join our [community channels](https://kx-community.slack.com/) to get assistance +- Request a feature in the [KX Community Ideas Forum](https://forum.kx.com/c/ideas-feature-requests/10). +- Review the latest [KDB-X Python Release Notes](release-notes/changelog.md). +- Ask for help in the [KX Community Slack](https://kx-community.slack.com/). diff --git a/docs/pykx-under-q/upgrade.md b/docs/pykx-under-q/upgrade.md index f12b55b..e76eb0f 100644 --- a/docs/pykx-under-q/upgrade.md +++ b/docs/pykx-under-q/upgrade.md @@ -10,7 +10,7 @@ tags: embedPy, KDB-X Python, q, _This page outlines differences and function mappings when upgrading from embedPy to KDB-X Python in a q session._ -Just like [KDB-X Python](../getting-started/what_is_pykx.md), [embedPy](https://github.com/kxsystems/embedpy) is a tool that allows to execute Python code and call Python functions. +Just like [KDB-X Python](../getting-started/what-is-kdb-x-python.md), [embedPy](https://github.com/kxsystems/embedpy) is a tool that allows to execute Python code and call Python functions. ## Functional differences @@ -152,4 +152,4 @@ x2| 0.486176 ## Next steps - Learn [how to use KDB-X Python within q](../pykx-under-q/intro.md). -- Use the [pykx.q Library Reference Card](../pykx-under-q/api.md). \ No newline at end of file +- Use the [pykx.q Library Reference Card](../pykx-under-q/api.md). diff --git a/docs/release-notes/changelog.md b/docs/release-notes/changelog.md index 6911bff..cdea8c4 100644 --- a/docs/release-notes/changelog.md +++ b/docs/release-notes/changelog.md @@ -1,20 +1,95 @@ -# KDB-X Python Changelog +# Changelog -This changelog tracks all KDB-X Python releases and their associated additions, fixes, and improvements. +_This changelog tracks KDB-X Python and PyKX releases, including additions, fixes, and improvements._ -!!! Note - For changes to KDB-X Python within a q environment, refer to the [KDB-X Python under q changelog](./underq-changelog.md). +!!! info "KDB-X Python and PyKX" + + **KDB-X Python** (`pykx>=4.0`) is the evolution of **PyKX** (`pykx<4.0`). + + This page groups release notes by product: + + - **KDB-X Python** (`pykx>=4.0`) + - **PyKX** (`pykx<4.0`) -## KDB-X + For guidance on migrating from PyKX to KDB-X Python, refer to the [migration guide](../upgrades/3040.md). -KDB-X is the evolution of kdb+. For full details and to install go [here](https://developer.kx.com/products/kdb-x/install). + +For changes to KDB-X Python within a q environment, refer to the [KDB-X Python under q changelog](./underq-changelog.md). ## KDB-X Python -KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details and a migration guide [here](../upgrades/3040.md). +### 4.1.0 + +**Release Date** + +2026-08-19 + +**Additions** + +- Added a reason why unlicensed mode was set when returning a `LicenseException` + + ```python + >>> import pykx as kx + >>> kx.q('1+1') + Traceback (most recent call last): + File "", line 1, in + File "/home/user/venv/lib/python3.10/site-packages/pykx/embedded_q.py", line 234, in __call__ + raise LicenseException("run q code via 'pykx.q'") + pykx.exceptions.LicenseException: Cannot run q code via 'pykx.q' as KDB-X Python is operating in unlicensed mode. Your license is corrupt or incompatible: + /tmp/license/k4.lic + ``` + +- Added `kx.util.qdebug()` to enable or disable q debugging at runtime. +- Added `kx.core.m9()`, which clears KDB-X Python's thread state memory. Refer to [multithreaded execution](../user-guide/advanced/threading.md) for more detail. + +**Fixes and Improvements** -## KDB-X Python 4.0.0 +- KDB-X Python now automatically manages the internal environment variables used to reimport it, so `PyKXReimport` (and `.pykx.safeReimport`) are not required in the majority of use cases. Refer to [Reimporting module](../api/reimporting.md) for more detail. +- `kx.toq` now converts a structured NumPy array (including a `numpy.recarray`, as produced by `pykx.Table.np`) into a `pykx.Table`, with one column per named field. Previously this raised `TypeError: Cannot convert ... to K object`. + + ```python + >>> import pykx as kx + >>> kx.toq(kx.q('([] c:enlist " ")').np()) + pykx.Table(pykx.q(' + c + - + + ')) + ``` + +- Fixed `QPATH` accumulating a duplicate `$QHOME/mod` entry on every nested KDB-X Python import. +- Fixed `.py()` conversion for large-magnitude `pykx.MinuteAtom` values. Before this fix, `.py()` returned an overflowed `datetime.timedelta` value or raised an error with NumPy 2.5 or later. +- `kx.schema.builder` no longer raises `pykx.exceptions.QError: sector` when you pass a `kx.SecondVector`. +- `kx.license.install` now installs license files that use nonstandard names and prints the destination path after a successful installation. + + ```python + >>> kx.license.install('~/.kx/kc_backup.lic', format='FILE', license_type='kc.lic') + KDB-X Python license successfully installed: /home/user/.kx/kc.lic + True + ``` + +- Opening a `kx.QConnection` directly now opens a single handle to the server. Previously each connection opened two. +- `kx.QConnection.close()` no longer leaks the socket and underlying q handle when the peer has already died. +- `kx.Table.rename` now ignores non-string column names instead of raising a `KeyError`. +- `kx.KeyedTable.reset_index` no longer raises `pykx.exceptions.QError: ]` when you pass integer `levels` with `drop=True`. +- The `is_null`, `is_pos_inf`, `is_neg_inf`, `has_nulls`, and `has_infs` properties now consistently return Python `bool` values (`True` or `False`). Previously, some types returned NumPy Boolean scalars. +- Restored the Conda release for ARM. +- Python 3.12 and 3.13 are now built for the `manylinux_2_28` platform (`x86_64` and `aarch64`) to support [NumPy 2.5.1](https://numpy.org/devdocs/release/2.5.1-notes.html#changes) and later. +- `kx.Column.scov` no longer raises `ValueError: setting an array element with a sequence`. +- Fixed `__array__` of atoms, vectors, and GUID arrays with NumPy 2.x, which passes `dtype` and `copy` arguments. Before this fix, `numpy.asarray(obj, dtype=...)` could raise `TypeError: __array__() takes 1 positional argument but 2 were given`. +- `.pd()` now builds DataFrames using a standard Pandas `BlockManager` on Pandas 3.0 and later. This fixes `.equals` behavior when you compare a DataFrame created by KDB-X Python with one created by Pandas. On Pandas 2.2 to 2.x the comparison remains asymmetric, as described in [known issues](../help/issues.md). +- `kx.toq` now preserves the symbol typing of empty or entirely-null Pandas string columns. +- Moved to [KDB-X 5.0.20260723](https://code.kx.com/kdb-x/releases/release-notes-latest.html). +- Fixed `pykx.exceptions.QError: The specified module could not be found.` raised on import on Windows. +- Fixed a segfault that could occur when converting some objects to `pykx.K` types in threads. +- `PYKX_KEEP_LOCAL_TIMES` now applies to Pandas temporal types. +- `q.read.csv` and `kx.QReader.csv` now correctly map `kx.DatetimeAtom` and `kx.DatetimeVector` to the q datetime type when you pass a list of column types. +- `kx.TimestampVector.py()` now correctly converts every null in the vector to `pandas.NaT`. +- `kx.TimestampVector.py(tzinfo=...)` no longer raises `AttributeError` when the vector contains nulls. +- `has_infs` now reports `True` for `kx.FloatVector` and `kx.RealVector` values containing negative infinity. + +### 4.0.0 **Release Date** @@ -23,7 +98,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a **Additions** - Added [`kx.use`](../api/module.md#pykx.module.use) for importing `q` modules within KDB-X Python. Refer to the [module documentation](../api/module.md) for more. - ```Python + ```python >>> import pykx as kx >>> pq = kx.use('kx.pq') >>> pq @@ -69,9 +144,102 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Moved to [KDB-X 5.0.20260501](https://code.kx.com/kdb-x/releases/release-notes-latest.html). - Fixed `.pykx.console` under q when [kxline](https://code.kx.com/kdb-x/how_to/general_guidance/embedded-line-editor-kxline.html) is in use. -- Includes all changes from PyKX [PyKX 3.1.8](#pykx-318), [PyKX 3.1.9](#pykx-319), and [PyKX 3.2.0](#pykx-320). +- Includes all changes from PyKX [PyKX 3.1.8](#318), [PyKX 3.1.9](#319), and [PyKX 3.2.0](#320). + +### 4.0.0b5 + +**Release Date** + +2026-02-09 + +**Additions** + +- New `VirtualTable` class for use with [Parquet module](https://code.kx.com/kdb-x/modules/parquet/overview.html): + + ```q + >>> import pykx as kx + >>> from pathlib import Path + >>> pq = kx.q.use('kx.pq') + >>> t = pq['pq'](Path('types.parquet')) + >>> t + pykx.VirtualTable(pykx.q('`T!`f`m`t!(k){[f;t;c;b;a;v]g:$[s:-1h=@b;0;#b];(bf;b1):$[g;df[t;0,0b;b];(::;()..')) + >>> t.select() + pykx.Table(pykx.q(' + col0 col1 col2 + --------------- + 1 1 "asd" + 1 2 "bsd" + 0 3 "csd" + 1 4 "asd" + 0 5 "bsd" + 0 6 "csd" + 1 7 "asd" + 0 8 "bsd" + 0 9 "csd" + ')) + >>> t.select(kx.Column('col1').max(), by=kx.Column('col2')) + pykx.KeyedTable(pykx.q(' + col2 | col1 + -----| ---- + "asd"| 7 + "bsd"| 8 + "csd"| 9 + ')) + ``` + +**Fixes and Improvements** + +- Moved to [KDB-X 5.0.20260122](https://code.kx.com/kdb-x/releases/release-notes-latest.html). +- Includes bug fixes from [PyKX 3.1.6](#316) and [PyKX 3.1.7](#317). -## PyKX 3.2.0 +### 4.0.0b4 + +**Release Date** + +2025-11-17 + +**Fixes and Improvements** + +- Moved to [KDB-X 5.0.20251114](https://code.kx.com/kdb-x/releases/release-notes-latest.html). +- Includes bug fixes from [PyKX 3.1.5](#315). + +### 4.0.0b3 + +**Release Date** + +2025-09-30 (expired 2026-01-04) + +**Fixes and Improvements** + +- Moved to KDB-X 0.1.1. + +### 4.0.0b2 + +**Release Date** + +2025-07-30 (expired 2025-09-30) + +**Fixes and Improvements** + +- Removes `invalid escape sequence` warnings upon import for some users. +- Turns on NumPy allocator by default. + - Replaces `PYKX_ALLOCATOR` with `PYKX_NO_ALLOCATOR` + - Replaces `--pykxalloc` with `--pykxnoalloc` +- Includes bug fixes from [PyKX 3.1.4](#314). + +### 4.0.0b1 + +**Release Date** + +2025-07-02 (expired 2025-09-30) + +**Additions** + +- Initial release. Uses [KDB-X 0.1.0](https://code.kx.com/kdb-x/releases/release-notes-latest.html). + +## PyKX + +### 3.2.0 **Release Date** @@ -483,7 +651,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Python 3.8 is no longer supported as it is [end of life](https://devguide.python.org/versions/#unsupported-versions). -## PyKX 3.1.9 +### 3.1.9 **Release Date** @@ -529,7 +697,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Updated 4.1 to 2026.04.01 for all platforms. -## PyKX 3.1.8 +### 3.1.8 **Release Date** @@ -540,7 +708,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Added `connection_timeout` keyword argument to `AsyncQConnection` objects allowing the specification of a timeout for the initial connection to a `q` server. - Added the usage of `.q.Q.ens` for `sym_enum` argument in `DB` objects. -## PyKX 3.1.7 +### 3.1.7 **Release Date** @@ -604,53 +772,8 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Deprecated the `value` keyword when creating `kx.Column` objects, use `data` instead. -## KDB-X Python 4.0.0b5 - -**Release Date** -2026-02-09 - -**Additions** - -- New `VirtualTable` class for use with [Parquet module](https://code.kx.com/kdb-x/modules/parquet/overview.html): - - ```q - >>> import pykx as kx - >>> from pathlib import Path - >>> pq = kx.q.use('kx.pq') - >>> t = pq['pq'](Path('types.parquet')) - >>> t - pykx.VirtualTable(pykx.q('`T!`f`m`t!(k){[f;t;c;b;a;v]g:$[s:-1h=@b;0;#b];(bf;b1):$[g;df[t;0,0b;b];(::;()..')) - >>> t.select() - pykx.Table(pykx.q(' - col0 col1 col2 - --------------- - 1 1 "asd" - 1 2 "bsd" - 0 3 "csd" - 1 4 "asd" - 0 5 "bsd" - 0 6 "csd" - 1 7 "asd" - 0 8 "bsd" - 0 9 "csd" - ')) - >>> t.select(kx.Column('col1').max(), by=kx.Column('col2')) - pykx.KeyedTable(pykx.q(' - col2 | col1 - -----| ---- - "asd"| 7 - "bsd"| 8 - "csd"| 9 - ')) - ``` - -**Fixes and Improvements** - -- Moved to [KDB-X 5.0.20260122](https://code.kx.com/kdb-x/releases/release-notes-latest.html). -- Includes bug fixes from [PyKX 3.1.6](#pykx-316) and [PyKX 3.1.7](#pykx-317). - -## PyKX 3.1.6 +### 3.1.6 **Release Date** @@ -663,7 +786,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a === "Behavior prior to change" - ```Python + ```python >>> da = kx.q.z.D >>> a = kx.toq(pd.DataFrame({'r':[2,3,4],'date':[da-2, da, da-1], 'k':[10, 11, 12]})) >>> b = kx.toq(pd.DataFrame({'r':[5,6,7],'date':[da-2, da-1, da], 'k':[13, 14, 15]})) @@ -679,7 +802,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a === "Behavior post change" - ```Python + ```python >>> da = kx.q.z.D >>> a = kx.toq(pd.DataFrame({'r':[2,3,4],'date':[da-2, da, da-1], 'k':[10, 11, 12]})) >>> b = kx.toq(pd.DataFrame({'r':[5,6,7],'date':[da-2, da-1, da], 'k':[13, 14, 15]})) @@ -693,18 +816,8 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a ')) ``` -## KDB-X Python 4.0.0b4 - -**Release Date** -2025-11-17 - -**Fixes and Improvements** - -- Moved to [KDB-X 5.0.20251114](https://code.kx.com/kdb-x/releases/release-notes-latest.html). -- Includes bug fixes from [PyKX 3.1.5](#pykx-315). - -## PyKX 3.1.5 +### 3.1.5 **Release Date** @@ -732,7 +845,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a === "Behavior prior to change" - ```Python + ```python >>> df=pd.DataFrame(dict(embeddings=list(np.random.ranf((500, 10)).astype(np.float32)))) >>> kx.toq(df) segfault @@ -740,7 +853,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a === "Behavior post change" - ```Python + ```python >>> df=pd.DataFrame(dict(embeddings=list(np.random.ranf((500, 10)).astype(np.float32)))) >>> kx.toq(df) ``` @@ -793,31 +906,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Creating more than one `DB` instance without specifying `overwrite=True` -## KDB-X Python 4.0.0b3 - -**Release Date** - -2025-09-30 (expired 2026-01-04) - -**Fixes and Improvements** - -- Moved to KDB-X 0.1.1. - -## KDB-X Python 4.0.0b2 - -**Release Date** - -2025-07-30 (expired 2025-09-30) - -**Fixes and Improvements** - -- Removes `invalid escape sequence` warnings upon import for some users. -- Turns on NumPy allocator by default. - - Replaces `PYKX_ALLOCATOR` with `PYKX_NO_ALLOCATOR` - - Replaces `--pykxalloc` with `--pykxnoalloc` -- Includes bug fixes from [PyKX 3.1.4](#pykx-314). - -## PyKX 3.1.4 +### 3.1.4 **Release Date** @@ -828,17 +917,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Resolved error on import when `PYKX_THREADING` was set. - Fixed an issue where data could be corrupted in keyed columns of PyArrow backed Pandas dataframes. -## KDB-X Python 4.0.0b1 - -**Release Date** - -2025-07-02 (expired 2025-09-30) - -**Additions** - -- Initial release. Uses [KDB-X 0.1.0](https://code.kx.com/kdb-x/releases/release-notes-latest.html). - -## PyKX 3.1.3 +### 3.1.3 **Release Date** @@ -1141,7 +1220,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Deprecated `kx.q.system.console_size`, use `kx.q.system.display_size` instead. -## PyKX 3.1.2 +### 3.1.2 **Release Date** @@ -1151,7 +1230,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Fixes an issue when using `PYKX_THREADING` that could cause a segfault in some scenarios. -## PyKX 3.1.1 +### 3.1.1 **Release Date** @@ -1190,7 +1269,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a ['dbsplay'] ``` -## PyKX 3.1.0 +### 3.1.0 **Release Date** @@ -1523,7 +1602,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a [0.6137, 0.5295, 0.6916, 0.2297, 0.6920]], dtype=torch.float64) ``` -## PyKX 3.0.1 +### 3.0.1 **Release Date** @@ -1609,7 +1688,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a ')) ``` -## PyKX 3.0.0 +### 3.0.0 **Release Date** @@ -1675,7 +1754,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - [Remote Function Execution](../user-guide/advanced/remote-functions.md) - [Streamlit Integration](../user-guide/advanced/streamlit.md) - [Multi-threaded use of PyKX](../user-guide/advanced/threading.md) - - Upgrade considerations for these features can be found [here](../upgrades/2030.md#remote-python-execution) + - Refer to [Remote Python execution](https://code.kx.com/pykx/3.0/upgrades/2030.html#remote-python-execution) in the PyKX 3.0 upgrade guide before you upgrade. - Allows user to overwrite jupyter notebook kernel with q code by setting `PYKX_JUPYTERQ` to `True` before starting the notebook. This can also be accessed at runtime using `#!python kx.util.jupyter_qfirst_enable()` and `#!python kx.util.jupyter_qfirst_disable()` - Tab completion for `reserved_words` list in `src/pykx/__init__.py` added for Jupyter Notebooks after the import of PyKX. @@ -1911,7 +1990,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Use the path to their already downloaded `kc.lic`/`k4.lic` licenses without going through the "Do you want to install a license" workflow - Allow users to persist for future use that they wish to use the IPC only unlicensed mode of PyKX, this will persist a file `~/.pykx-config` which sets configuration denoting unlicensed mode is to be used. -- Addition of function `#!python kx.util.install_q` to allow users who do not have access to a `q` executable at the time of installing PyKX. See [here](../getting-started/installing.md) for instructions regarding its use +- Addition of function `#!python kx.util.install_q` to allow users who do not have access to a `q` executable at the time of installing PyKX. See [here](../user-guide/advanced/streaming/index.md#before-you-start) for instructions regarding its use - Addition of function `#!python kx.util.start_q_subprocess` to allow a `q` process to be started on a specified port with supplied initialisation arguments, for example: ```python @@ -1985,7 +2064,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a **Fixes and Improvements** -- Previously built-in `#!python help()` python function, when called on q keywords such as `#!python kx.q.rand` or `#!python kx.q.max` was displaying the generic `wrappers` object help message. As noted in [the installation docs](../getting-started/installing.md#dependencies), this functionality requires the installation of optional libraries available through `pip install pykx[help]`. +- Previously built-in `#!python help()` python function, when called on q keywords such as `#!python kx.q.rand` or `#!python kx.q.max` was displaying the generic `wrappers` object help message. As noted in [the dependency reference](../user-guide/configuration.md#dependencies-and-bundled-assets), this functionality requires the installation of optional libraries available through `pip install pykx[help]`. === "Behavior prior to change" @@ -2054,7 +2133,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a ``` - Previously loading pykx.q during q startup using `QINIT` or `QHOME/q.q` resulted in a segfault or a corruption. -- Removal of several deprecated configuration variables which previously noted to be deprecated at the next major release. See [here](../upgrades/2030.md#deprecations) for more information. +- Removed several configuration variables marked as deprecated in earlier releases. Refer to [Deprecations](https://code.kx.com/pykx/3.0/upgrades/2030.html#deprecations) in the PyKX 3.0 upgrade guide. - Attempts to convert `#!python kx.List` objects with non-conforming types previously resulted in an ambiguous error, updated error message indicates the root cause. === "Behavior prior to change" @@ -2315,7 +2394,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a === "Behavior post change" - ```Python + ```python >>> t = kx.q('([] g:2?0Ng)') >>> kx.toq(t['g'].pd(raw=True)) pykx.GUIDVector(pykx.q('8c6b8b64-6815-6084-0a3e-178401251b68 5ae7962d-49f2-404d-5aec-f7c8abbae288')) @@ -2675,7 +2754,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - PyKX 3.0.0 is a major version update which includes changes requiring review before upgrading from 2.5.*. A page details these changes in full [here](../upgrades/2030.md) -## PyKX 2.5.5 +### 2.5.5 **Release Date** @@ -2686,7 +2765,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - PyKX Pandas dependency has been raised to allow `<=2.2.3` for Python>3.8 - PyKX Pandas dependency for Python 3.8 has been clamped to `<2.0` due to support being dropped for it by Pandas after 2.0.3. -## PyKX 2.5.4 +### 2.5.4 **Release Date** @@ -2743,7 +2822,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Version 2.5.4 marks the removal of support for releases to PyPI/Anaconda of Python 3.7 supported versions of PyKX -## PyKX 2.5.2 +### 2.5.2 **Release Date** @@ -2855,7 +2934,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Reworked `Table.std()` method to better handle edge cases relating to mixed columns and nulls. Now matching Pandas results. This addresses issues raised [here](https://github.com/KxSystems/pykx/issues/28). - Fix to issue where loading PyKX on Windows from 2.5.0 could result in a users working directory being changed to `site-packages/pykx`. -## PyKX 2.5.1 +### 2.5.1 **Release Date** @@ -3029,7 +3108,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Updated CSV analysis logic to be based on `csvutil.q` 2020.06.20. - Fix for config value `PYKX_4_1_ENABLED` to only use 4.1 if set to `True`, `true`, or `1`. Previously any non empty value enabled 4.1. -## PyKX 2.5.0 +### 2.5.0 **Release Date** @@ -3535,7 +3614,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Addition of [streamlit](https://streamlit.io/) connection class `pykx.streamlit.Connection` to allow querying of q processes when building a streamlit application. For an example of this functionality and an introduction to it's usage see [here](../user-guide/advanced/streamlit.md). -## PyKX 2.4.2 +### 2.4.2 **Release Date** @@ -3545,7 +3624,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Updated `libq` to 2024.03.28 for all supported OS's. -## PyKX 2.4.1 +### 2.4.1 **Release Date** @@ -3582,7 +3661,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a 5e-324 ``` -## PyKX 2.4.0 +### 2.4.0 **Release Date** @@ -3894,7 +3973,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a warn('PYKX_THREADING is only supported on Linux / MacOS, it has been disabled.') ``` -## PyKX 2.3.2 +### 2.3.2 **Release Date** @@ -3905,7 +3984,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - Update of PyKX 4.0 linux shared object to version 2024.02.09, this update is to facilitate deployments on more secure linux/linux-arm environments. - Update `Table.rename()` to skip over columns not in table instead of throwing error to match `pandas`. -## PyKX 2.3.1 +### 2.3.1 **Release Date** @@ -4018,7 +4097,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a >>> remote_session.clear() ``` -## PyKX 2.3.0 +### 2.3.0 **Release Date** @@ -4131,7 +4210,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a When using `PYKX_THREADING` you must ensure you call `kx.shutdown_thread()` at the end of the script to ensure the background thread is properly closed. -## PyKX 2.2.3 +### 2.2.3 **Release Date** @@ -4149,7 +4228,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a PyKX 2.2.3 is currently not available for Mac x86 for all Python versions, additionally it is unavailable for Mac ARM on Python 3.7. Updated builds will be provided once available. -## PyKX 2.2.2 +### 2.2.2 !!! Warning @@ -4165,7 +4244,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - A regression in 2.2.1 resulted in `SIGINT` signals being incorrectly treated as `SIGTERM` style signals, PyKX now resets all signals overwritten by PyKX to their values prior to import. - Indexing regression in 2.2.1 causing hangs for certain inputs such as `tbl[::-1]` has been resolved. -## PyKX 2.2.1 +### 2.2.1 !!! Warning @@ -4316,7 +4395,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a - If dependent on the environment variable `UNDER_PYTHON` please upgrade your code to use `PYKX_UNDER_PYTHON` -## PyKX 2.2.0 +### 2.2.0 **Release Date** @@ -4630,7 +4709,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a pykx.CharAtom(pykx.q('"a"')) ``` -## PyKX 2.1.2 +### 2.1.2 **Release Date** @@ -4664,7 +4743,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a pykx.LongAtom(pykx.q('2')) ``` -## PyKX 2.1.1 +### 2.1.1 **Release Date** @@ -4680,7 +4759,7 @@ KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details a AttributeError: module 'pykx' has no attribute 'PyKXSerialized' ``` -## PyKX 2.1.0 +### 2.1.0 **Release Date** @@ -4818,7 +4897,7 @@ the following reads a CSV file and specifies the types of the three columns name - The version of `Cython` used to build `PyKX` was updated to the full `3.0.x` release version. -## PyKX 2.0.1 +### 2.0.1 **Release Date** @@ -4830,7 +4909,7 @@ the following reads a CSV file and specifies the types of the three columns name - Use of the environment variables `QARGS='--unlicensed'` or `QARGS='--licensed'` operate correctly following regression in 2.0.0 - Fix to issue where `OSError` would be raised when `close()` was called on an IPC connection which has already disconnected server side -## PyKX 2.0.0 +### 2.0.0 **Release Date** @@ -4982,7 +5061,7 @@ the following reads a CSV file and specifies the types of the three columns name - Pandas API functionality is enabled permanently which will modify data indexing and retrieval. Users should ensure to review and test their codebase before upgrading. -## PyKX 1.6.3 +### 1.6.3 **Release Date** @@ -4999,7 +5078,7 @@ the following reads a CSV file and specifies the types of the three columns name - Prevent attempting to pass wrapped Python functions over IPC. - Support IPC payloads over 4GiB. -## PyKX 1.6.2 +### 1.6.2 **Release Date** @@ -5019,7 +5098,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fix error when binding with [FFI](https://github.com/KxSystems/ffi) in `QINIT`. - Fix issue calling `peach` with `PYKX_RELEASE_GIL` set to true when calling a Python function. -## PyKX 1.6.1 +### 1.6.1 **Release Date** @@ -5044,7 +5123,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed an issue where reimporting `PyKX` when run under q would cause a segmentation fault. - Updated the warning message for the insights core libraries failing to load to make it more clear that no error has occurred. -## PyKX 1.6.0 +### 1.6.0 **Release Date** @@ -5096,7 +5175,7 @@ the following reads a CSV file and specifies the types of the three columns name ')) ``` -## PyKX 1.5.3 +### 1.5.3 **Release Date** @@ -5107,7 +5186,7 @@ the following reads a CSV file and specifies the types of the three columns name - Added support for Pandas `Float64Index`. - Wheels for ARM64 based Macs are now available for download. -## PyKX 1.5.2 +### 1.5.2 **Release Date** @@ -5117,7 +5196,7 @@ the following reads a CSV file and specifies the types of the three columns name - Added support for ARM 64 Linux. -## PyKX 1.5.1 +### 1.5.1 **Release Date** @@ -5128,7 +5207,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed an issue with `pykx.q` that caused errors to not be raised properly under q. - Fixed an issue when using `.pykx.get` and `.pykx.getattr` that caused multiple calls to be made. -## PyKX 1.5.0 +### 1.5.0 **Release Date** @@ -5153,7 +5232,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed a bug around conversions of `Pandas` tables with no column names. - Fixed an issue around `.pykx.qeval` not returning unwrapped results in certain scenarios. -## PyKX 1.4.2 +### 1.4.2 **Release Date** @@ -5163,7 +5242,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed an issue that would cause `EmbeddedQ` to fail to load. -## PyKX 1.4.1 +### 1.4.1 **Release Date** @@ -5175,7 +5254,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed a memory leak around calling wrapped `Foreign` objects in `pykx.q`. - Fixed an issue around the `tls` keyword argument when creating `QConnection` instances, as well as a bug in the unlicensed behavior of `SecureQConnection`'s. -## PyKX 1.4.0 +### 1.4.0 **Release Date** @@ -5209,7 +5288,7 @@ the following reads a CSV file and specifies the types of the three columns name - Added support for a Pandas like API around `Table` and `KeyedTable` instances, documentation for the specific functionality can be found [here](../user-guide/advanced/Pandas_API.ipynb). - Added `.pykx.setdefault` to `pykx.q` which allows the default conversion type to be set without using environment variables. -## PyKX 1.3.2 +### 1.3.2 **Release Date** @@ -5219,7 +5298,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed support for using TLS with `SyncQConnection` instances. -## PyKX 1.3.1 +### 1.3.1 **Release Date** @@ -5242,7 +5321,7 @@ the following reads a CSV file and specifies the types of the three columns name - OpenSSLv3 support - Added ability to specify maximum length for IPC error messages. The default is 256 characters and this can be changed by setting the `PYKX_MAX_ERROR_LENGTH` environment variable. -## PyKX 1.3.0 +### 1.3.0 **Release Date** @@ -5261,7 +5340,7 @@ the following reads a CSV file and specifies the types of the three columns name - EmbedPy replacement functionality `pykx.q` updated significantly to provide parity with embedPy from a syntax perspective. Documentation of the interface [here](../pykx-under-q/intro.md) provides API usage. Note that initialization requires the first version of Python to be retrieved on a users `PATH` to have PyKX installed. Additional flexibility with respect to installation location is expected in `1.4.0` please provide any feedback to `pykx@kx.com` -## PyKX 1.2.2 +### 1.2.2 **Release Date** @@ -5271,7 +5350,7 @@ the following reads a CSV file and specifies the types of the three columns name - Fixed an issue causing the timeout argument for `QConnection` instances to not work properly. -## PyKX 1.2.1 +### 1.2.1 **Release Date** @@ -5282,7 +5361,7 @@ the following reads a CSV file and specifies the types of the three columns name - Added support for OpenSSLv3 for IPC connections created when in 'licensed' mode. - Updated conversion functionality for timestamps to support conversions within Pandas 1.5.0 -## PyKX 1.2.0 +### 1.2.0 **Release Date** @@ -5310,7 +5389,7 @@ the following reads a CSV file and specifies the types of the three columns name - Support for [NEP-49](https://numpy.org/neps/nep-0049.html), which allows Numpy arrays to be converted into `q` Vectors without copying the underlying data. This behavior is opt-in and you can do so by setting the environment variable `PYKX_ALLOCATOR` to 1, "1" or True or by adding the flag `--pykxalloc` to the `QARGS` environment variable. Note: This feature also requires a python version of at least 3.8. - Support the ability to trigger early garbage collection of objects in the `q` memory space by adding `--pykxgc` to the QARGS environment variable, or by setting the `PYKX_GC` environment variable to 1, "1" or True. -## PyKX 1.1.1 +### 1.1.1 **Release Date** @@ -5320,7 +5399,7 @@ the following reads a CSV file and specifies the types of the three columns name - Added ability to skip symlinking `$QHOME` to `PyKX`'s local `$QHOME` by setting the environment variable `IGNORE_QHOME`. -## PyKX 1.1.0 +### 1.1.0 **Release Date** @@ -5355,7 +5434,7 @@ the following reads a CSV file and specifies the types of the three columns name - Improved performance of converting `pykx.SymbolVector` to `numpy.array` of strings, and also the conversion back from a `numpy.array` of `strings` to a `q` `SymbolVector`. - Improved performance of converting `numpy.array`'s of `dtype`s `datetime64`/`timedelta64 ` to the various `pykx.TemporalTypes`. -## PyKX 1.0.1 +### 1.0.1 **Release Date** @@ -5378,7 +5457,7 @@ the following reads a CSV file and specifies the types of the three columns name - Misc test updates. - Misc doc updates. -## PyKX 1.0.0 +### 1.0.0 **Release Date** diff --git a/docs/release-notes/deprecations.md b/docs/release-notes/deprecations.md index e5a0bf6..6de6a76 100644 --- a/docs/release-notes/deprecations.md +++ b/docs/release-notes/deprecations.md @@ -24,7 +24,7 @@ A list of deprecated behaviors and the version in which they were removed. For f ## PyKX 3.1.7 -- Deprecated `value` keyword when creating `Column` objects, use `data` instead +- Deprecated `value` keyword when creating `Column` objects, use `data` instead. ## PyKX 3.1.5 @@ -113,4 +113,4 @@ Release Date: 2025-02-11 - 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. -- The `pykx.kdb` singleton class has been removed. \ No newline at end of file +- The `pykx.kdb` singleton class has been removed. diff --git a/docs/release-notes/underq-changelog.md b/docs/release-notes/underq-changelog.md index 0ad6e74..7e0f83d 100644 --- a/docs/release-notes/underq-changelog.md +++ b/docs/release-notes/underq-changelog.md @@ -14,6 +14,19 @@ KDB-X is the evolution of kdb+. For full details and to install, refer to the [K KDB-X Python `pykx>=4.0` is the evolution of PyKX `pykx<4.0`. For more details, refer to the migration guide [here](../upgrades/3040.md). +## KDB-X Python 4.1.0 + +**Release Date** + +2026-08-19 + +**Fixes and Improvements** + +- `.pykx.safeReimport` is no longer required in the majority of use cases. Refer to [Reimporting module](../api/reimporting.md) for more detail. +- Loading KDB-X Python under q no longer exports `PYKX_SKIP_UNDERQ`, so a Python process spawned from that q session imports with the full `.pykx` API rather than inheriting a skipped under-q integration. +- `.pykx.setdefault` now only changes the default conversion for the current process; it no longer exports `PYKX_DEFAULT_CONVERSION` (which previously leaked the value to child processes). +- Fixed q projections passed to a Python function being evaluated during argument parsing. + ## PyKX 3.2.0 **Release Date** diff --git a/docs/scripts/collapse-code.js b/docs/scripts/collapse-code.js new file mode 100644 index 0000000..92db1e7 --- /dev/null +++ b/docs/scripts/collapse-code.js @@ -0,0 +1,65 @@ +document$.subscribe(function () { + const THRESHOLD = 25; + const COLLAPSE_LINES = 10; + + document.querySelectorAll("pre code").forEach(function (code) { + const pre = code.parentElement; + + if (pre.closest(".code-compare, .code-compare-three")) return; + + const lines = code.textContent.split("\n").filter(function (l, i, arr) { + return !(i === arr.length - 1 && l === ""); + }); + if (lines.length < THRESHOLD) return; + + const wrapper = document.createElement("div"); + wrapper.className = "code-collapse"; + pre.parentNode.insertBefore(wrapper, pre); + wrapper.appendChild(pre); + + const lineHeight = parseFloat(getComputedStyle(code).lineHeight); + const collapsedHeight = lineHeight * COLLAPSE_LINES; + pre.style.maxHeight = collapsedHeight + "px"; + pre.style.overflow = "hidden"; + + const toggle = document.createElement("button"); + toggle.className = "code-collapse-toggle code-collapse-toggle--bottom"; + toggle.setAttribute("aria-expanded", "false"); + toggle.textContent = "▼ Show all " + lines.length + " lines"; + wrapper.appendChild(toggle); + + function expand() { + pre.style.maxHeight = "none"; + pre.style.overflow = ""; + toggle.setAttribute("aria-expanded", "true"); + toggle.textContent = "▲ Show fewer lines"; + wrapper.classList.add("code-collapse--expanded"); + } + + function collapse() { + pre.style.maxHeight = collapsedHeight + "px"; + pre.style.overflow = "hidden"; + toggle.setAttribute("aria-expanded", "false"); + toggle.textContent = "▼ Show all " + lines.length + " lines"; + wrapper.classList.remove("code-collapse--expanded"); + } + + toggle.addEventListener("click", function () { + pre.style.overflow === "hidden" ? expand() : collapse(); + }); + + // Auto-expand if search.highlight injects a inside a collapsed block. + // MutationObserver catches marks added after collapse runs (search nav is async). + if (pre.querySelector("mark")) { + expand(); + } else { + var observer = new MutationObserver(function () { + if (pre.querySelector("mark")) { + expand(); + observer.disconnect(); + } + }); + observer.observe(pre, { childList: true, subtree: true }); + } + }); +}); diff --git a/docs/scripts/init_kapa_widget.js b/docs/scripts/init_kapa_widget.js new file mode 100644 index 0000000..1ba381a --- /dev/null +++ b/docs/scripts/init_kapa_widget.js @@ -0,0 +1,115 @@ +document.addEventListener("DOMContentLoaded", function () { + var script = document.createElement("script"); + script.src = "https://widget.kapa.ai/kapa-widget.bundle.js"; + + // ── Core / identity ─────────────────────────────────────────────── + script.setAttribute("data-website-id", "90e95ec3-2524-43ab-93da-3e94fdd01d59"); + script.setAttribute("data-project-name", "KX"); + script.setAttribute("data-modal-title", "Ask AI"); + + // ── Branding: logos ─────────────────────────────────────────────── + // Launcher / button logo: + script.setAttribute("data-project-logo", "https://kx.com/files/kx-logo-dark.png"); + script.setAttribute("data-project-logo-dark", "https://kx.com/files/kx-logo-white.png"); + // Modal (answer window) logo — the widget reads `-src` (dark handled via the + // generic `-dark` suffix). Bare `data-modal-logo` is not read by the widget. + // Pick the initial `-src` based on the page's active MkDocs color scheme: + // white logo when slate (dark) is active, otherwise the dark logo. + // NB: read the ACTIVE scheme off — the palette toggle renders hidden + // inputs each carrying a `data-md-color-scheme` attribute, so a generic + // `[data-md-color-scheme='slate']` query always matches and misreports light. + var isSlate = document.body.getAttribute("data-md-color-scheme") === "slate"; + script.setAttribute( + "data-modal-logo-src", + isSlate + ? "https://kx.com/files/kx-logo-white.png" + : "https://kx.com/files/kx-logo-dark.png" + ); + //script.setAttribute("data-modal-logo-src-dark", "https://kx.com/files/kx-logo-white.png"); + + // ── Branding: accent color (gold in light, blue in dark) ────────── + // Drives the primary/brand accent (e.g. buttons). Legacy attribute, + // retained until kapa exposes a dedicated semantic accent token. + script.setAttribute("data-project-color", "#38a8e0"); + script.setAttribute("data-project-color-dark", "#FFC300"); + script.setAttribute("data-button-text-color", "#060000"); + + // Some components (the Deep Thinking toggle when enabled, and the Stop + // button) derive their color from the *base* `data-project-color` inline and + // ignore `data-project-color-dark`, so they stay blue in dark mode. Component + // styles have no `-dark` variant, so we apply gold overrides only when slate + // is the active scheme at load (pages reload on navigation, so this stays + // correct without a live observer). + if (isSlate) { + script.setAttribute("data-deep-thinking-button-enabled-color", "#FFC300"); + script.setAttribute("data-deep-thinking-button-enabled-background-color", "rgba(255, 195, 0, 0.1)"); + script.setAttribute("data-deep-thinking-button-enabled-border-color", "#FFC300"); + // Submit / stop button (solid-fill icon button) — gold fill in dark. + script.setAttribute("data-submit-button-background-color", "#FFC300"); + } + + // ── Layout & sizing ─────────────────────────────────────────────── + script.setAttribute("data-view-mode", "sidebar"); + script.setAttribute("data-modal-open-by-default", "false"); + script.setAttribute("data-modal-z-index", "10001"); + script.setAttribute("data-modal-size", "495px"); + script.setAttribute("data-scale-factor", "0.8"); + script.setAttribute("data-modal-title-font-size", "18px"); + + // ── Theme: color scheme (default light; follow MkDocs slate → dark) ─ + script.setAttribute("data-color-scheme", "light"); + script.setAttribute("data-color-scheme-selector", "[data-md-color-scheme='slate']"); + + // ── Theme: semantic tokens — dark ──────────────────────────────── + script.setAttribute("data-surface-color-dark", "#141417"); + script.setAttribute("data-surface-elevated-color-dark", "#1f1f24"); + script.setAttribute("data-surface-hover-color-dark", "#2a2a31"); + script.setAttribute("data-text-color-dark", "#e8e8ec"); + script.setAttribute("data-text-muted-color-dark", "#9ca3af"); + script.setAttribute("data-border-color-dark", "#4a4a52"); + script.setAttribute("data-anchor-color-dark", "#B8860B"); // darkened gold: legible link color on dark + script.setAttribute("data-deep-thinking-button-enabled-color-dark", "#B8860B"); + script.setAttribute("data-deep-thinking-button-hover-color-dark", "#B8860B"); + script.setAttribute("data-deep-thinking-button-enabled-hover-color-dark", "#B8860B"); + script.setAttribute("data-answer-feedback-button-enabled-border-dark", "1px solid #B8860B"); + script.setAttribute("data-answer-feedback-button-enabled-color-dark", "#B8860B"); + script.setAttribute("data-answer-feedback-button-enabled-hover-color-dark", "#B8860B"); + + // ── Theme: semantic tokens — light ──────────────────────────────── + script.setAttribute("data-surface-color", "#ffffff"); + script.setAttribute("data-surface-elevated-color", "#f8f9fa"); + script.setAttribute("data-surface-hover-color", "#f1f3f5"); + script.setAttribute("data-text-color", "#111111"); + script.setAttribute("data-text-muted-color", "#666666"); + script.setAttribute("data-border-color", "#dee2e6"); + script.setAttribute("data-anchor-color", "#5ab8f0"); // blue: matches light accent + script.setAttribute("data-deep-thinking-button-enabled-color", "#5ab8f0"); + script.setAttribute("data-deep-thinking-button-hover-color", "#5ab8f0"); + script.setAttribute("data-deep-thinking-button-enabled-hover-color", "#5ab8f0"); + script.setAttribute("data-deep-thinking-button-text-hover-color", "#5ab8f0"); + script.setAttribute("data-answer-feedback-button-enabled-border", "1px solid #5ab8f0"); + script.setAttribute("data-answer-feedback-button-enabled-color", "#5ab8f0"); + script.setAttribute("data-answer-feedback-button-enabled-hover-color", "#5ab8f0"); + + // ── Features: MCP & search ──────────────────────────────────────── + script.setAttribute("data-mcp-enabled", "true"); + script.setAttribute("data-mcp-server-url", "https://kx.mcp.kapa.ai"); + script.setAttribute("data-search-mode-enabled", "true"); + script.setAttribute("data-search-source-ids-order", "c2743780-4e53-447c-a7f9-b12c419907c2"); + + // ── Analytics & consent ─────────────────────────────────────────── + script.setAttribute("data-user-analytics-fingerprint-enabled", "false"); + script.setAttribute("data-user-analytics-cookie-enabled", "false"); + script.setAttribute("data-consent-required", "false"); + script.setAttribute("data-consent-screen-title", "Help us improve our AI assistant"); + script.setAttribute("data-consent-screen-disclaimer", "By clicking 'Allow tracking', you consent to anonymous user tracking which helps us improve our service. We don't collect any personally identifiable information."); + script.setAttribute("data-consent-screen-accept-button-text", "Allow tracking"); + script.setAttribute("data-consent-screen-reject-button-text", "No, thanks"); + + // ── Content: disclaimer & example questions ─────────────────────── + script.setAttribute("data-modal-disclaimer", "This is an experimental custom LLM for answering technical questions about KX products. Answers are based **only** on KX documentation and support sources, but may not be fully accurate so please use your best judgement. Don't forget to rate the answers and give feedback!"); + script.setAttribute("data-modal-example-questions", "How do I get started with KDB-X Python?,What's new in KDB-X Python (pykx)?,How do I get started with KDB-X?,How do I learn q?"); + + script.async = true; + document.head.appendChild(script); +}); diff --git a/docs/scripts/search-highlight-clear.js b/docs/scripts/search-highlight-clear.js new file mode 100644 index 0000000..90a29f4 --- /dev/null +++ b/docs/scripts/search-highlight-clear.js @@ -0,0 +1,19 @@ +// Clear search.highlight marks on first click after they appear. +// Marks are injected asynchronously after navigation, so a MutationObserver +// watches for them rather than polling with a fixed timeout. +document$.subscribe(function () { + var content = document.querySelector(".md-content") || document.body; + + var observer = new MutationObserver(function () { + if (!content.querySelector("mark")) return; + observer.disconnect(); + document.addEventListener("click", function clearMarks() { + content.querySelectorAll("mark").forEach(function (mark) { + mark.replaceWith(document.createTextNode(mark.textContent)); + }); + document.removeEventListener("click", clearMarks); + }); + }); + + observer.observe(content, { childList: true, subtree: true }); +}); diff --git a/docs/scripts/strip-q-prompt-copy.js b/docs/scripts/strip-q-prompt-copy.js index 42e9b9a..ad99aa5 100644 --- a/docs/scripts/strip-q-prompt-copy.js +++ b/docs/scripts/strip-q-prompt-copy.js @@ -1,8 +1,8 @@ // When a user clicks a code copy button, copy the code but remove -// leading REPL prompts (q), >>>, and Python continuation ...). +// leading REPL and shell prompts (q), >>>, Python continuation ..., and $). // -// Add this file to mkdocs extra_javascript as javascript/strip-repl-prompts.js -// and ensure it loads after the Material copy button script. +// Loaded from mkdocs.yml as scripts/strip-q-prompt-copy.js. +// Ensure it loads after the Material copy button script. (function () { @@ -17,10 +17,11 @@ // Regex to remove leading REPL prompts (allow optional leading whitespace) // Matches: - // q) - // >>> - // ... - const REPL_PROMPT_RE = /^\s*(q\)|>>>|\.\.\.)\s?/; + // q) + // >>> + // ... + // $ + const REPL_PROMPT_RE = /^\s*(?:(?:q\)|>>>|\.\.\.)\s?|\$\s+)/; // Helper: find nearest ancestor matching a selector function closest(el, selector) { @@ -146,5 +147,4 @@ }); }); - })(); - \ No newline at end of file + })(); diff --git a/docs/spelling.txt b/docs/spelling.txt index 53129bd..ed43856 100644 --- a/docs/spelling.txt +++ b/docs/spelling.txt @@ -110,3 +110,4 @@ doesn builtins kobject streamlit +jupyter diff --git a/docs/stylesheets/2026-edited.css b/docs/stylesheets/2026-edited.css index 41fb386..ea15fea 100644 --- a/docs/stylesheets/2026-edited.css +++ b/docs/stylesheets/2026-edited.css @@ -25,6 +25,11 @@ --md-typeset-img-bg: #f4edda; --md-typeset-mark-color: rgba(295,195,0,.3); /*KX Yellow: #FFC300*/ } + + /* Show the diagram variant that matches the active color scheme. */ + [data-md-color-scheme="kx-light"] .only-dark { display: none; } + [data-md-color-scheme="slate"] .only-light { display: none; } + .md-typeset a:hover { color: var(--md-typeset-a-color); } @@ -296,11 +301,13 @@ border: 1px solid var(--kx-black); border-radius: 10px; } -.md-typeset div.large-tile-grid > div h2 { +.md-typeset div.large-tile-grid > div h2, +.md-typeset div.large-tile-grid > div h3 { margin: 0; font-weight: 500; } -.md-typeset div.large-tile-grid > div h2 span { +.md-typeset div.large-tile-grid > div h2 span, +.md-typeset div.large-tile-grid > div h3 span { display: block; margin-top: 0.3rem; font-size: 0.9rem; @@ -360,4 +367,74 @@ .md-tabs__link { user-select: none; -} \ No newline at end of file +} + +/* Collapsible code blocks */ +.code-collapse { + position: relative; +} + +.code-collapse::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 6em; + background: linear-gradient(to bottom, transparent 0%, var(--md-code-bg-color) 80%); + pointer-events: none; +} + +[data-md-color-scheme="slate"] .code-collapse::after { + background: linear-gradient(to bottom, transparent, var(--kx-blue-grey)); +} + +.code-collapse--expanded::after { + display: none; +} + +.code-collapse-toggle--bottom { + position: absolute; + bottom: 2em; + left: 50%; + transform: translateX(-50%); + margin-bottom: 0; + z-index: 1; + background: var(--md-code-bg-color); +} + +[data-md-color-scheme="slate"] .code-collapse-toggle--bottom { + background: var(--kx-blue-grey); +} + +[data-md-color-scheme="kx-light"] .code-collapse-toggle--bottom { + background: var(--md-code-bg-color); +} + +.code-collapse-toggle { + display: inline-block; + margin-bottom: 0.4em; + cursor: pointer; + color: var(--kx-blue); + background: none; + border: 1px solid var(--kx-blue); + border-radius: 0.2em; + font-weight: 500; + font-size: 0.8em; + padding: 0.2em 0.6em; +} + +.code-collapse-toggle:hover { + background: var(--kx-blue); + color: white; +} + +[data-md-color-scheme="slate"] .code-collapse-toggle { + color: var(--kx-yellow); + border-color: var(--kx-yellow); +} + +[data-md-color-scheme="slate"] .code-collapse-toggle:hover { + background: var(--kx-yellow); + color: var(--kx-black); +} diff --git a/docs/stylesheets/404.css b/docs/stylesheets/404.css new file mode 100644 index 0000000..fdc1c4a --- /dev/null +++ b/docs/stylesheets/404.css @@ -0,0 +1,266 @@ +/* KDB-X Python 404 page. Keep selectors scoped for easy reuse. */ +.md-main__inner:has(.not-found-page) { + margin-top: 0; +} + +.md-content__inner:has(> .not-found-page) { + margin-top: 0; + padding-top: 0; +} + +.md-content__inner:has(> .not-found-page)::before { + display: none; +} + +.not-found-page { + --not-found-accent: #194387; + --not-found-accent-hover: #123469; + --not-found-border: #d9e1e8; + --not-found-card-background: var(--md-default-bg-color); + --not-found-card-border: var(--kx-black, #0b0d0e); + --not-found-card-text: var(--kx-blue, #0f62fe); + --not-found-heading: #68727d; + --not-found-link: #194387; + --not-found-muted: #56616f; + margin: 0 auto 1rem; + max-width: 66rem; +} + +[data-md-color-scheme="slate"] .not-found-page { + --not-found-accent: #ffc300; + --not-found-accent-hover: #ffcf33; + --not-found-border: #3b444d; + --not-found-card-background: var(--kx-black, #0b0d0e); + --not-found-card-border: var(--kx-yellow, #ffc300); + --not-found-card-text: var(--kx-yellow, #ffc300); + --not-found-heading: #9ba3aa; + --not-found-link: #ffc300; + --not-found-muted: #bac3c9; +} + +.md-typeset .not-found-page h1, +.md-typeset .not-found-page h2, +.md-typeset .not-found-page p { + margin-top: 0; +} + +.not-found-hero { + align-items: center; + display: grid; + gap: clamp(1.75rem, 4vw, 3.25rem); + grid-template-columns: minmax(11rem, 0.7fr) minmax(20rem, 1.3fr); + min-height: 0; + padding: 0.5rem 0 1.75rem; +} + +.not-found-copy, +.not-found-support-group { + min-width: 0; +} + +.not-found-illustration { + aspect-ratio: 4 / 3; + background: url("../img/404/not-found-light.svg") center / contain no-repeat; + justify-self: center; + max-width: 15rem; + width: 100%; +} + +[data-md-color-scheme="slate"] .not-found-illustration { + background-image: url("../img/404/not-found-dark.svg"); +} + +.md-typeset .not-found-copy h1 { + color: var(--not-found-heading); + font-size: 2em; + font-weight: 300; + letter-spacing: -0.025em; + line-height: 1.15; + margin: 0 0 0.85rem; +} + +.not-found-copy > p:not(.not-found-code) { + color: var(--md-typeset-color); + line-height: 1.45; + max-width: 33rem; +} + +.not-found-copy .not-found-lead { + color: var(--md-typeset-color); + font-weight: 400; + line-height: 1.45; +} + +.not-found-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0.9rem; +} + +.md-typeset .not-found-actions .md-button { + background-color: var(--not-found-accent); + border-color: var(--not-found-accent); + border-radius: 0.3rem; + color: #fff; + font-weight: 400; + margin: 0; +} + +.md-typeset .not-found-actions .md-button:hover, +.md-typeset .not-found-actions .md-button:focus-visible { + background-color: var(--not-found-accent-hover); + border-color: var(--not-found-accent-hover); + color: #fff; +} + +[data-md-color-scheme="slate"] .md-typeset .not-found-actions .md-button, +[data-md-color-scheme="slate"] .md-typeset .not-found-actions .md-button:hover, +[data-md-color-scheme="slate"] .md-typeset .not-found-actions .md-button:focus-visible { + color: #101820; +} + +.not-found-lower { + display: grid; + grid-template-columns: minmax(0, 1fr); +} + +.not-found-support { + background: transparent; + border-top: 1px solid var(--not-found-border); + display: grid; + gap: 1.5rem 2.5rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 1.6rem; + padding: 1.1rem 0 0; +} + +.md-typeset .not-found-destinations h2 { + font-size: 1.25em; + font-weight: 400; + margin: 0 0 0.35rem; +} + +.md-typeset .not-found-support h2 { + color: var(--md-typeset-color); + font-size: 1em; + font-weight: 400; + line-height: 1.4; + margin: 0 0 0.35rem; +} + +.not-found-support-links { + display: flex; + flex-wrap: wrap; + gap: 0.2rem 1rem; +} + +.md-typeset .not-found-support-group > a, +.md-typeset .not-found-support-links a { + color: var(--not-found-link); + font-weight: 400; +} + +.md-typeset .not-found-support-group > a:hover, +.md-typeset .not-found-support-group > a:focus-visible, +.md-typeset .not-found-support-links a:hover, +.md-typeset .not-found-support-links a:focus-visible { + color: var(--not-found-link); + text-decoration: underline; +} + +.not-found-destinations { + padding: 0; +} + +.not-found-destinations-intro { + color: var(--not-found-muted); + margin-bottom: 0; +} + +.not-found-card-grid { + display: grid; + gap: 1rem 1.25rem; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 1.1rem; +} + +.md-typeset .not-found-card { + align-items: center; + background: var(--not-found-card-background); + border: 1px solid var(--not-found-card-border); + border-radius: 10px; + color: var(--not-found-card-text); + display: flex; + min-height: 2.9rem; + padding: 0.65rem 0.85rem; +} + +.md-typeset .not-found-card-title { + color: var(--not-found-card-text); + font-weight: 400; + line-height: 1.3; +} + +.md-typeset .not-found-card:focus-visible { + outline: 2px solid var(--not-found-card-text); + outline-offset: 2px; +} + +@media screen and (max-width: 56rem) { + .not-found-hero { + gap: 2rem; + grid-template-columns: minmax(13rem, 0.75fr) minmax(18rem, 1.25fr); + } + + .not-found-card-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media screen and (max-width: 42rem) { + .not-found-page { + margin-top: 0; + } + + .not-found-hero, + .not-found-support { + grid-template-columns: minmax(0, 1fr); + } + + .not-found-hero { + gap: 1rem; + padding: 0 0 1.5rem; + } + + .not-found-illustration { + justify-self: center; + max-width: 10rem; + } + + .not-found-copy { + text-align: center; + } + + .not-found-copy > p:not(.not-found-code) { + margin-left: auto; + margin-right: auto; + } + + .not-found-actions { + align-items: stretch; + flex-direction: column; + justify-content: center; + } + + .md-typeset .not-found-actions .md-button { + text-align: center; + width: 100%; + } +} + +@media screen and (max-width: 30rem) { + .not-found-card-grid { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/docs/stylesheets/prism-edited.css b/docs/stylesheets/prism-edited.css index 2eb16d2..66a0be5 100644 --- a/docs/stylesheets/prism-edited.css +++ b/docs/stylesheets/prism-edited.css @@ -55,12 +55,12 @@ pre[class*="language-"] { pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { - background: #073642; /* base02 */ + background: var(--md-typeset-mark-color); } pre[class*="language-"]::selection, pre[class*="language-"] ::selection, code[class*="language-"]::selection, code[class*="language-"] ::selection { - background: #073642; /* base02 */ + background: var(--md-typeset-mark-color); } /* Code blocks */ @@ -120,7 +120,6 @@ pre[class*="language-"] { .token.entity { color: #657b83; /* base00 */ - background: #eee8d5; /* base2 */ } .token.atrule, diff --git a/docs/upgrades/2030.md b/docs/upgrades/2030.md index d6bf898..394a392 100644 --- a/docs/upgrades/2030.md +++ b/docs/upgrades/2030.md @@ -9,143 +9,4 @@ tags: PyKX, upgrade, remote Python execution # Upgrade from PyKX 2.5.* to 3.* _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3.*._ -## API Changes - -### Remote Python execution - -- Remote Python Execution is no longer a Beta feature. To use this feature, remove the setting of the `#!python PYKX_BETA_FEATURES` environment variable. - -- Additional required dependencies for this feature are now part of the required dependencies. - - === "Previous behavior" - - ```bash - pip install pykx[beta] - ``` - - === "New behavior" - - ```bash - pip install pykx - ``` - -- Generation of a remote session which can be used previously was a two-step process: - - 1. Initialize the session object - 1. Create the session - - This changed to a single function call. - - === "Previous behavior" - - ```python - >>> import pykx as kx - >>> session = kx.remote.session() - >>> session.create(host='localhost', port=5050) - ``` - - === "New behavior" - - ```python - >>> import pykx as kx - >>> session = kx.remote.session(host='localhost', port=5050) - ``` - -- How users specify the Python libraries which should be available on remote processes has changed: - - - Previously this was done using a function call to `#!python session.add_library`. This function would specify the libraries to be loaded on first execution of the function and expected the names of the libraries to be loaded as a list of arguments. - - Now you can use the keyword `#!python libraries` at session creation to load the libraries. Also, the library addition function is now called `session.libraries` to match the API for streaming with PyKX. Finally the `#!python libraries` keyword and function take a dictionary mapping the aliased name for the library to the library which is to be imported, namely `#!python import numpy as np` would be defined as `#!python {'np': 'numpy'}`. - - === "Previous Behavior" - - ```python - >>> import pykx as kx - >>> session = kx.remote.session() - >>> session.create(host='localhost', port=5050) - >>> session.add_library('numpy', 'pykx') - ``` - - === "New Behavior" - - ```python - >>> import pykx as kx - # Initialise libraries at session creation - >>> session = kx.remote.session(port=5050, libraries = {'kx': 'pykx', 'np': 'numpy'}) - - # Add Libraries after session creation - >>> session = kx.remote.session(port=5050) - >>> session.libraries({'kx': 'pykx', 'np': 'numpy'}) - ``` - -- The `#!python clear` method provided for `#!python session` objects is now called `#!python close`. This change aligns the naming with IPC communication channels being 'closed' when stopping communication with a remote session and aligns with the naming used within the IPC module - - === "Previous Behavior" - - ```python - >>> import pykx as kx - >>> session = kx.remote.session() - >>> session.create(host='localhost', port=5050) - >>> session.clear() - ``` - - === "New Behavior" - - ```python - >>> import pykx as kx - >>> session = kx.remote.session(host='localhost', port=5050) - >>> session.close() - ``` - -## Deprecations - -- The following table outlines environment variables/configuration options which are now fully deprecated and the updated name for these values if they exist. - - | **Deprecated option** | **Supported option** | - | :----------------------- | :---------------------- | - | `PYKX_NO_SIGINT` | `PYKX_NO_SIGNAL` | - | `IGNORE_QHOME` | `PYKX_IGNORE_QHOME` | - | `KEEP_LOCAL_TIMES` | `PYKX_KEEP_LOCAL_TIMES` | - | `SKIP_UNDERQ` | `PYKX_SKIP_UNDERQ` | - | `UNDER_PYTHON` | `PYKX_UNDER_PYTHON` | - | `UNSET_PYKX_GLOBALS` | No longer applicable | - | `PYKX_UNSET_GLOBALS` | No longer applicable | - | `PYKX_ENABLE_PANDAS_API` | No longer applicable | - -- Removal of the now deprecated `#!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 be use `#!python inplace`. -- Removal of the deprecated `#!python replace_self` keyword when attempting to overwrite a `#!python pykx.Table` or `#!python KeyedTable` using insert/upsert functionality. To maintain this behavior use the `#python inplace` keyword. - -## Error message changes - -Various `#!python pykx.QError` error messages now provide more verbose explanations for users. Any code which relies on specific error string returns may need to be updated, some messages below are truncated for display purposes. - -| **Previous error message** | **Updated error message** | -| :--------------------------- | :-------------------------------------------------------------------------------- | -| `access` | `access: Failed to connect to server with invalid username/password` | -| `par` | `par: Cannot execute an unsupported operation on a partitioned table or its ...` | -| `splay` | `splay: Cannot execute an unsupported operation on a splayed table` | -| `assign` | `assign: Cannot redefine reserved q word` | -| `insert` | `insert: Cannot insert a record with an existing key into a keyed table` | -| `s-fail` | `s-fail: Cannot set "sorted" attribute on an unsorted list ...` | -| `u-fail` | `u-fail: Failed to do one of the following: ...` | -| `no-update` | `noupdate: Cannot update a global variable while using: ...` | -| `no-socket` | `nosocket: Cannot open or use a socket on a thread other than main. ...` | - -## Null and Infinite conversion changes - -PyKX previously left some null and infinite values unconverted, now these are converted to native Python objects. -The behavior of Atom and Vector conversions has also been updated to more closely match each other. - -The links below outline the full before and after behavior. - -- [Null Conversions](../user-guide/fundamentals/nulls_and_infinities.md#null-conversions). -- [Infinite Conversions](../user-guide/fundamentals/nulls_and_infinities.md#infinite-conversions). - -## Pandas 2.2.X Update - -PyKX now works with Pandas 2.2.X, introducing some breaking changes in behavior. Specifically, the `.equals` method now checks the `_mgr` type of DataFrames, which can result in unilateral behavior when comparing PyKX and Pandas objects. - -These changes may affect compatibility with code written for earlier versions of Pandas. - -The link below outline the full details of the changes and their implications. - -- [Pandas Changes](../help/issues.md#pandas). \ No newline at end of file +Refer to the [PyKX 2.5.* to 3.* upgrading guide](https://code.kx.com/pykx/3.0/upgrades/2030.html) in the 3.0 version of this site for full information. \ No newline at end of file diff --git a/docs/user-guide/advanced/compress-encrypt.md b/docs/user-guide/advanced/compress-encrypt.md index 0354b49..3858dc7 100644 --- a/docs/user-guide/advanced/compress-encrypt.md +++ b/docs/user-guide/advanced/compress-encrypt.md @@ -177,19 +177,19 @@ Notice the information about the persistence characteristics of your data using ```python >>> kx.q('-21!`:/tmp/db/2020.01.01/tab/x') -pykx.Dictionary(pykx.q('')) +pykx.Dictionary(pykx.q('(`symbol$())!()')) >>> kx.q('-21!`:/tmp/db/2020.01.02/tab/x') pykx.Dictionary(pykx.q(' -compressedLength | 5467 -uncompressedLength| 8016 +compressedLength | 53551 +uncompressedLength| 80016 algorithm | 2i logicalBlockSize | 17i zipLevel | 4i ')) >>> kx.q('-21!`:/tmp/db/2020.01.03/tab/x') pykx.Dictionary(pykx.q(' -compressedLength | 6374 -uncompressedLength| 8016 +compressedLength | 59625 +uncompressedLength| 80016 algorithm | 4i logicalBlockSize | 17i zipLevel | 10i @@ -212,7 +212,7 @@ To set the `#!python gzip` globally, use the `#!python global_init` on the gener ```python >>> compress = kx.Compress(algo=kx.CompressionAlgorithm.gzip, level=9) >>> compress.global_init() ->>> kx.q.z.z.d +>>> kx.q.z.zd pykx.LongVector(pykx.q('17 2 9')) ``` diff --git a/docs/user-guide/advanced/database/db_gen.md b/docs/user-guide/advanced/database/db_gen.md index 99cc903..dae0ae6 100644 --- a/docs/user-guide/advanced/database/db_gen.md +++ b/docs/user-guide/advanced/database/db_gen.md @@ -93,7 +93,7 @@ Now that you have generated a database, you can add extra partitions using the s ... 'price': kx.random.random(N, 10.0) ... }).pd() >>> type(trade) -pandas.core.frame.DataFrame + ``` Note that in comparison to the original database creation logic, we do not have a `#!python date` column. Instead, we add a date at partition creation. Below we provide a variety of examples of adding new partitions under various conditions: @@ -157,7 +157,7 @@ We can now add this data to your database ```python >>> db.create(quote, 'quote', format='splayed') >>> db.tables - ['trade', 'quote'] + ['quote', 'trade'] >>> type(db.quote) ``` diff --git a/docs/user-guide/advanced/database/db_mgmt.md b/docs/user-guide/advanced/database/db_mgmt.md index 37dc282..58d95f2 100644 --- a/docs/user-guide/advanced/database/db_mgmt.md +++ b/docs/user-guide/advanced/database/db_mgmt.md @@ -51,11 +51,11 @@ In the below cell, we complete the following: 1. Make a copy of the column `#!python price` named `#!python price_copy`. 1. Adjust the value of the stock price on the copied column to account for a two-for-one stock split by multiplying the price by half. 1. Delete the original `#!python price` column. -1. Rename the copied column `#!python symbol_copy` to be `#!python symbol`. +1. Rename the copied column `#!python price_copy` to be `#!python price`. 1. Reorder the columns. ```python ->>> col_order = db.trade.columns.py() +>>> col_order = db.list_columns('trade') >>> db.copy_column('trade', 'price', 'price_copy') >>> db.apply_function('trade', 'price_copy', lambda x: x * 0.5) >>> db.delete_column('trade', 'price') diff --git a/docs/user-guide/advanced/ipc.md b/docs/user-guide/advanced/ipc.md index bffed89..69a13da 100644 --- a/docs/user-guide/advanced/ipc.md +++ b/docs/user-guide/advanced/ipc.md @@ -41,9 +41,10 @@ In the below sections you will learn more about these connections and how to ```python >>> import pykx as kx >>> import subprocess + >>> import time >>> with kx.PyKXReimport(): ... server = subprocess.Popen( - ... ('python', 'server.py'), + ... ('python', 'server.py', '5050'), ... stdin=subprocess.PIPE, ... stdout=subprocess.DEVNULL, ... stderr=subprocess.DEVNULL, diff --git a/docs/user-guide/advanced/modes.md b/docs/user-guide/advanced/modes.md index ebaa73f..de90f15 100644 --- a/docs/user-guide/advanced/modes.md +++ b/docs/user-guide/advanced/modes.md @@ -55,7 +55,7 @@ This mode cannot run q embedded within it. Also, it lacks the ability to run q c ### 1.b Running in Licensed mode -Licensed mode is the standard way to operate KDB-X Python, wherein it's running under a Python process [with a valid q license](../../getting-started/installing.md#2-install-a-license). This modality aims to replace PyQ as the Python-first library for KX. All KDB-X Python features are available in this mode. +Licensed mode is the standard way to operate KDB-X Python, wherein it's running under a Python process [with a valid q license](../../getting-started/installing.md#install-a-kdb-x-license). This modality aims to replace PyQ as the Python-first library for KX. All KDB-X Python features are available in this mode. The differences provided through operating with a valid license are: diff --git a/docs/user-guide/advanced/remote-functions.md b/docs/user-guide/advanced/remote-functions.md index 98a9e2e..838452a 100644 --- a/docs/user-guide/advanced/remote-functions.md +++ b/docs/user-guide/advanced/remote-functions.md @@ -121,14 +121,14 @@ Examples of each of these methods can be seen below: ```python >>> import pykx as kx - >>> session = kx.remote.session(port=5050, libraries={'kx': pykx}) + >>> session = kx.remote.session(port=5050, libraries={'kx': 'pykx'}) ``` === "Library addition functionality" ```python >>> session.libraries({'np': 'numpy', 'kx': 'pykx'}) - >>> @function(session) + >>> @kx.remote.function(session) ... def dependent_function(x, y, z): ... return kx.q.mavg(4, np.linspace(x, y, z)) >>> dependent_function(0, 10, 10) @@ -138,7 +138,7 @@ Examples of each of these methods can be seen below: === "Defining imports within function body" ```python - >>> @function(remote_session) + >>> @kx.remote.function(session) ... def dependent_function(x, y, z): ... import pykx as kx ... import numpy as np diff --git a/docs/user-guide/advanced/serialization.md b/docs/user-guide/advanced/serialization.md index bbe1a53..20a2d90 100644 --- a/docs/user-guide/advanced/serialization.md +++ b/docs/user-guide/advanced/serialization.md @@ -106,7 +106,7 @@ Similar to the examples in the previous section in the below we will serialize a x x1 x2 ------- 1 2 3 - >>> print(kx.deserialize(sertab.copy()) + >>> print(kx.deserialize(sertab.copy())) x x1 x2 ------- 1 2 3 diff --git a/docs/user-guide/advanced/streaming/index.md b/docs/user-guide/advanced/streaming/index.md index 686b1f0..9708abd 100644 --- a/docs/user-guide/advanced/streaming/index.md +++ b/docs/user-guide/advanced/streaming/index.md @@ -13,9 +13,13 @@ _This page is an introduction to the KDB-X Python Real-Time Data Capture functio The capture, persistence and presentation of high velocity real-time data provides significant challenges to users at all levels from new users attempting to capture this form of data for the first time, to seasoned data-engineers building complex ingestion workflows. -!!! Note "Install q" +## Before you start - The Real-Time Data Capture functionality provided by KDB-X Python requires you to have access to a `q` executable. A workflow is provided by KDB-X Python to install q as outlined [here](../../../getting-started/installing.md). Alternatively, you will be prompted to install q if not detected when initializing the Real-Time Capture functionality. +Real-Time Capture starts separate q processes and requires a q executable. Most other KDB-X Python features use the embedded q runtime and do not require this step. + +[Install KDB-X](https://code.kx.com/kdb-x/get_started/kdb-x-install.html) to get the required `q` executable. + +If the Python process cannot resolve `q`, [configure the q executable path](../../configuration.md#configure-q-for-real-time-capture). The KDB-X Python Real-Time Data Capture functionality described in this documentation provides a framework for users at all levels of their journey to build highly performant real-time systems which can quickly provide users with the following: diff --git a/docs/user-guide/advanced/threading.md b/docs/user-guide/advanced/threading.md index e0d93b5..9d83e52 100644 --- a/docs/user-guide/advanced/threading.md +++ b/docs/user-guide/advanced/threading.md @@ -31,6 +31,7 @@ Before globally enabling this functionality, consider the following: - **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. +- **Thread States**: When using `#!python PYKX_THREADING`, when using threads you must ensure that you call `pykx.core.m9()` before closing the thread to ensure the thread state is released and memory is not leaked. ## How to enable multithreaded execution diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index d360c68..bfa3377 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -1,18 +1,27 @@ --- -title: Configure KDB-X Python -description: How to configure KDB-X Python -date: July 2024 -author: KX Systems, Inc., +title: Configure KDB-X Python +description: Set environment variables, customize runtime behavior, and configure advanced options for KDB-X Python. +last_updated: August 2026 +author: KX Systems, Inc., a subsidiary of KX Software Limited tags: KDB-X Python, data, convert --- -# Configure KDB-X Python +# Configure KDB-X Python -_This page provides details on how to configure KDB-X Python using a configuration file and/or environment variables._ +_Configure KDB-X Python with a configuration file or environment variables, and look up every option it accepts._ -To modify the underlying behavior of KDB-X Python based on your specific use-case, check out your [options](#options) for use-case/performance tuned optimizations of the library. You can configure them using a [configuration file](#configuration-file) or [environment variables](#environment-variables) as described below. +Set the [configuration options](#options) in a [configuration file](#configuration-file) or with [environment variables](#environment-variables). -!!! warning "Important: In all cases, environment variable definitions take precedence over definitions within the configuration file." +!!! warning "Configuration precedence" + + Environment variables take precedence over values in the configuration file. + +## Prerequisites + +Before you start, make sure you have: + +- [KDB-X Python installed](../getting-started/installing.md#install-from-pypi) +- A [KDB-X license](../getting-started/installing.md#install-a-kdb-x-license), if you intend to use options that require [licensed mode](advanced/modes.md) ## Configuration file @@ -31,14 +40,21 @@ PYKX_RELEASE_GIL="true" PYKX_BETA_FEATURES="true" ``` -On import of KDB-X Python, the file `#!python config-pykx` is searched for according to the following path ordering. The first found file is used for definition of the KDB-X Python configuration: +When you import KDB-X Python, it searches these locations in order and uses the first `#!python config-pykx` file it finds: | **Order** | **Location** | |-----------|--------------------------------------------------| | 1. | `Path(os.getenv('PYKX_CONFIGURATION_LOCATION'))` | | 2. | `Path.home()/'.kx/config-pykx'` | -When loading this file, unless otherwise specified, KDB-X Python uses the profile `#!python default`. To configure non-default profiles from within this file, set an environment variable `#!python PYKX_PROFILE` prior to loading of KDB-X Python, for example using the above configuration file. +KDB-X Python loads the `#!python default` profile unless you name another one. To select a different profile, set `#!python PYKX_PROFILE` before you import KDB-X Python – for example, using the configuration file above. + +Two variables control which file and profile KDB-X Python loads. Because KDB-X Python reads them before it reads the file, set them as environment variables only: + +| **Variable** | **Default** | **Description** | +| --- | --- | --- | +| `PYKX_CONFIGURATION_LOCATION` | Unset | Path to the configuration file to load, checked before `~/.kx/config-pykx`. | +| `PYKX_PROFILE` | `default` | Name of the profile within the configuration file to load. | === "default" @@ -58,7 +74,7 @@ When loading this file, unless otherwise specified, KDB-X Python uses the profil True ``` -To add values to your configuration file you can modify the file directly or alternatively use the helper function `#!python kx.util.add_to_config` as follows for example +To add values, edit the file directly or call the helper function `#!python kx.util.add_to_config`: ```python >>> import pykx as kx @@ -73,7 +89,7 @@ Successfully added: ## Environment variables -If you wish to configure the [options](#options) as environment variables, before importing KDB-X Python, you can, for example, define the environment variables to use: +To set the [options](#options) as environment variables, define them before you import KDB-X Python: ```python >>> import os @@ -84,87 +100,283 @@ If you wish to configure the [options](#options) as environment variables, befor True ``` +## Check the active configuration + +Because environment variables override the configuration file, the value in effect is not always the one you set most recently. To see what KDB-X Python resolved, call `#!python kx.util.debug_environment`: + +```python +>>> import pykx as kx +>>> kx.util.debug_environment() +``` + +It reports which file and profile KDB-X Python loaded, along with the value of every option: + +```text +**** KDB-X Python Configuration File **** +File location: /usr/local/.kx/config-pykx +Used profile: default +Profile content: {'PYKX_Q_EXECUTABLE': '/usr/local/anaconda3/envs/qenv/q/m64/q'} + +**** KDB-X Python Configuration Variables **** +PYKX_IGNORE_QHOME: False +PYKX_KEEP_LOCAL_TIMES: False +PYKX_NO_ALLOCATOR: False +PYKX_GC: False +``` + +The function also reports your Python, platform, license, and q details, which makes its full output a useful attachment for a support request. Refer to [`pykx.util.debug_environment`](../api/util.md#pykxutildebug_environment) for the complete output and the `detailed` and `return_info` arguments. + +!!! tip "Check the variable, not the attribute" + + `#!python kx.config` also exposes resolved settings as attributes, but their names differ from the variables and `PYKX_NO_ALLOCATOR` is inverted: `#!python kx.config.k_allocator` reads `#!python True` when `PYKX_NO_ALLOCATOR` is unset. `#!python kx.util.debug_environment` prints the variable names themselves, so prefer it when confirming a setting. + +## Configuration tasks + +### Configure q for Real-Time Capture + +[Real-Time Capture](advanced/streaming/index.md) starts q subprocesses. If the Python process cannot resolve the `q` command, set these values in the [configuration file](#configuration-file) or as [environment variables](#environment-variables): + +| **Variable** | **Value** | +| --- | --- | +| `PYKX_Q_EXECUTABLE` | Full path to `QHOME/[lmw]64/q[.exe]` | +| `QHOME` | Directory where q is installed | + +Set the values before importing KDB-X Python. + +Those q subprocesses also inherit `QHOME` from the Python process. If they fail to start – particularly under kdb+ 4.0/4.1 – refer to [QHOME, symlinking, and subprocesses](#qhome-symlinking-and-subprocesses) and use `PYKX_IGNORE_QHOME` or [`PyKXReimport`](../api/reimporting.md). + +### QHOME, symlinking, and subprocesses + +By default, KDB-X Python takes over the `QHOME` environment variable when running in licensed mode, pointing it at an internal directory bundled with the `pykx` package rather than your own q installation. So the embedded q can still see the scripts and data in your original `QHOME`, KDB-X Python symlinks its contents into that internal directory. It deliberately excludes the bootstrap files `q.k` and `s.k_` from this symlinking. + +This affects any q subprocess started after importing KDB-X Python – it inherits the internal `QHOME`, not your original one: + +| **q subprocess** | **Result** | +|---|---| +| Python that imports KDB-X Python | Works – your original `QHOME` is restored automatically on import. | +| KDB-X 5.0 `q` | Works – KDB-X 5.0 doesn't need `q.k` and can find its own installation automatically. Note that `QHOME` is still *respected* if set, so pointing it at a directory with no license can still cause a license error. | +| kdb+ 4.0/4.1 `q` | Fails to start – kdb+ requires `QHOME` to point at an installation containing `q.k`, with no auto-discovery fallback. | + +On Windows, symlinking often requires elevated privileges. If it fails, the internal `QHOME` may be missing your license too, so either q flavor can fail to start. + +#### Run q subprocesses + +If you need to start a q subprocess – particularly kdb+ 4.0/4.1 – choose one of the following: + +- **`PYKX_IGNORE_QHOME="true"`** – Set this before importing KDB-X Python to stop it from modifying `QHOME`. If you've set `QHOME` yourself, KDB-X Python leaves it untouched for this process and any q subprocess. + + !!! warning + If you *haven't* set `QHOME`, KDB-X Python still writes a fallback value (`~/.kx` if it exists, otherwise its own internal directory) into the environment. On a machine with neither a `QHOME` nor a `~/.kx`, a kdb+ subprocess can still inherit the internal `QHOME`. To guarantee a real `QHOME` for a kdb+ subprocess, set `QHOME` explicitly. + +- **[`PyKXReimport`](../api/reimporting.md)** – Restores your original `QHOME` around a single q subprocess launch, then reverts to KDB-X Python's internal `QHOME`. Use this if you want KDB-X Python's default behavior everywhere except for specific q subprocess launches. + +!!! note + With `PYKX_IGNORE_QHOME="true"`, the embedded q no longer has access to scripts and data in your `QHOME` (that's what the symlinking normally provides). Put any q scripts you need on `QPATH` instead. + +## Dependencies and bundled assets + +### Required Python packages + +`pip` installs these packages with KDB-X Python and selects versions compatible with the active Python version: + +| **Package** | **Purpose** | +| --- | --- | +| [NumPy](https://pypi.org/project/numpy) | Converts KDB-X Python objects to NumPy arrays and supports NumPy functions on KDB-X Python data. | +| [pandas](https://pypi.org/project/pandas) | Converts data to pandas `Series` and `DataFrame` objects and supports some PyArrow conversions. | +| [pytz](https://pypi.org/project/pytz/) | Applies time-zone offsets during temporal data conversion. | +| [toml](https://pypi.org/project/toml/) | Parses the `config-pykx` configuration file. | +| [dill](https://pypi.org/project/dill) | Serializes Python objects for [remote functions](advanced/remote-functions.md) and [Real-Time Capture](advanced/streaming/index.md). | +| [requests](https://pypi.org/project/requests/) | Provides HTTP client functionality. | + +### Optional Python packages + +Install an optional dependency group with `python -m pip install "pykx[]"`. + +| **Extra** | **Package** | **Purpose** | +| --- | --- | --- | +| `pyarrow` | `pyarrow>=3.0.0` | Converts KDB-X Python objects to and from PyArrow tables and arrays. | +| `debug` | `find-libpython~=0.2` | Locates the `libpython` library that [KDB-X Python under q](../pykx-under-q/intro.md) requires. | +| `streaming` | `psutil>=5.0.0` | Manages the q subprocesses that Real-Time Capture starts. | +| `dashboards` | `ast2json~=0.3` | Supports KX Dashboards Direct integration. | +| `streamlit` | `streamlit~=1.28` | Supports Streamlit integration. | +| `torch` | `torch>2.1` | Converts between `torch.Tensor` objects and KDB-X Python objects on supported platforms. | + +### Optional system libraries + +- `libssl` supports TLS on [IPC connections](../api/ipc.md). +- `libpthread` supports `PYKX_THREADING` on Linux and macOS. + +### Bundled assets + +KDB-X Python wheels include these native libraries: + +| **Platform** | **Mode** | **File** | **Version** | +| --- | --- | --- | --- | +| Linux ARM | KDB-X | `libq.so` | 5.0.20260706 | +| Linux x86 | KDB-X | `libq.so` | 5.0.20260706 | +| macOS ARM | KDB-X | `libq.dylib` | 5.0.20260706 | +| macOS x86 | KDB-X | `libq.dylib` | 5.0.20260706 | +| Windows | KDB-X | `q.dll`, `q.lib` | 5.0.20260706 | +| Linux ARM | Unlicensed | `libe.so` | 2023.11.22 | +| Linux x86 | Unlicensed | `libe.so` | 2023.11.22 | +| macOS ARM | Unlicensed | `libe.so` | 2023.11.22 | +| macOS x86 | Unlicensed | `libe.so` | 2023.11.22 | +| Windows | Unlicensed | `e.dll`, `e.lib` | 2024.08.21 | + ## Options -You have various options to tune KDB-X Python behavior at run time. You must set these variables before importing KDB-X Python. They remain effective throughout the execution of the KDB-X Python process. - -### General - -To enable or disable advanced features of KDB-X Python across all modes of operation, use the following variables: - -| **Option** | **Default** | **Values** | **Description** | -|---------------------------------|-------------|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `PYKX_BETA_FEATURES` | `False` | `1` or `true` | Enable all Beta features supplied with KDB-X Python allowing users to test and prototype code slated for later releases. | -| `PYKX_QDEBUG` | `False` | `1` or `true` | Enable retrieval of backtrace information on error being raised when executing q functions, this can alternatively be enabled by setting `debug=True` as a keyword in calls to `kx.q`. | -| `PYKX_IGNORE_QHOME` | `False` | `1` or `true` | When loading KDB-X Python on a machine with an existing q installation (and the environment variable `QHOME` set to the installation folder), KDB-X Python will look within this directory for q scripts their dependencies. It will then symlink these files to make them available to load under KDB-X Python. This variable instructs KDB-X Python to not perform this symlinking. | -| `PYKX_KEEP_LOCAL_TIMES` | `False` | `1` or `true` | When converting a Python datetime object to q, KDB-X Python will translate the Python datetime into UTC before the conversion. This variable instructs KDB-X Python to convert the Python datetime using the local time zone. | -| `PYKX_NO_ALLOCATOR` | `False` | `1` or `true` | KDB-X Python implements [NEP-49](https://numpy.org/neps/nep-0049.html) which allows q to handle memory allocation of all Numpy arrays so they can be converted more efficiently to q. This avoids the need to resort to a copy where possible. Use `PYKX_NO_ALLOCATOR` to turn this behaviour off, resulting in full data copies. | -| `PYKX_GC` | `False` | `1` or `true` | KDB-X Python can trigger q garbage collector when Numpy arrays allocated by KDB-X Python are deallocated. This variable enables this behavior which will release q memory to the OS following deallocation of the Numpy array at the cost of a small overhead. Only usable when `PYKX_NO_ALLOCATOR` is not set. | -| `PYKX_LOAD_PYARROW_UNSAFE` | `False` | `1` or `true` | By default, KDB-X Python uses a subprocess to import pyarrow as it can result in a crash when the version of pyarrow is incompatible. This variable will trigger a normal import of pyarrow and importing KDB-X Python should be slightly faster. | -| `PYKX_MAX_ERROR_LENGTH` | `256` | size in characters | By default, KDB-X Python reports IPC connection errors with a message buffer of size 256 characters. This allows the length of these error messages to be modified reducing the chance of excessive error messages polluting logs. | -| `PYKX_QCE` | `False` | `1` or `true` | On Linux, KDB-X Python comes with q Cloud Edition features from [Insights Core](https://code.kx.com/insights/core/). This variable allows a user to opt in the loading of q Cloud Edition functionality. | -| `PYKX_Q_LIB_LOCATION` | `UNSET` | Path to a directory containing q libraries necessary for loading KDB-X Python | See [here](../release-notes/changelog.md#pykx-131) for detailed information. This allows a user to store the KDB-X Python libraries: `q.so`, `q.k` etc. separately from their Python installation. This is required for some enterprise use-cases. **Note:** If using this override you must manage the folder to ensure its contents match the version of KDB-X Python you are using | -| `PYKX_RELEASE_GIL` | `False` | `1` or `true` | When PYKX_RELEASE_GIL is enabled the Python Global Interpreter Lock will not be held when calling into q. | -| `PYKX_Q_LOCK` | `False` | `1` or `true` | When PYKX_Q_LOCK is enabled a re-entrant lock is added around calls into q, this lock will stop multiple threads from calling into q at the same time. This allows embedded q to be thread safe even when using PYKX_RELEASE_GIL. | -| `PYKX_DEBUG_INSIGHTS_LIBRARIES` | `False` | `1` or `true` | If the insights libraries failed to load this variable can be used to print out the full error output for debugging purposes. | -| `PYKX_UNLICENSED` | `False` | `1` or `true` | Set KDB-X Python to make use of the library in `unlicensed` mode at all times. | -| `PYKX_LICENSED` | `False` | `1` or `true` | Set KDB-X Python to make use of the library in `licensed` mode at all times. If licensed initialisation fails the import will error rather than bringing up the interactive license helper. Fallback to unlicensed mode is blocked. | -| `PYKX_THREADING` | `False` | `1` or `true` | When importing KDB-X Python start EmbeddedQ within a background thread. This allows calls into q from any thread to modify state, this environment variable is only supported for licensed users. | -| `PYKX_NO_SIGNAL` | `False` | `1` or `true` | Skip overwriting of [signal](https://docs.python.org/3/library/signal.html) definitions by KDB-X Python, these are presently overwritten by default to reset Pythonic default definitions with are reset by KDB-X Python on initialisation in licensed modality. | -| `PYKX_JUPYTERQ` | `False` | `1` or `true` | When enabled, any Jupyter Notebook will start in q first mode by default when KDB-X Python is imported. | -| `PYKX_Q_EXECUTABLE` | `q` | string denoting path to q executable | This allows users to specify the location of the q executable which should be called when using making use of the `tick` module for defining streaming infrastructures | -| `PYKX_SUPPRESS_WARNINGS` | `False` | `1` or `true` | This allows the user to suppress warnings that have been suggested as sensible to be raised by users for KDB-X Python in situations where edge cases can result in unexpected behavior. Warnings in scenarios where a decision has been made to not support behavior explicitly rather than where user discretion is required are still maintained. | -| `PYKX_CONFIGURATION_LOCATION` | `.` | The path to the folder containing the `config-pykx` file. | This allows users to specify a location other than the `.` or a users `home` directory to store their configuration file outlined [here](#configuration-file) | -| `PYKX_CONFIGURATION_PROFILE` | `default` | The "profile" defined in `config-pykx` file to be used. | Users can specify which set of configuration variables are to be used by modifying the `PYKX_CONFIGURATION_PROFILE` variable see [here](#configuration-file) for more details. Note that this configuration can only be used as an environment variable. | - -To set the environment for q (embedded in KDB-X Python, in licensed mode), use the variables below: - -| **Variable** | **Values** | **Description** | -|--------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| -| `QARGS` | See link | Command-line flags to pass to q, see [here](https://code.kx.com/q/basics/cmdline/) for more information. | -| `QHOME` | Path to the users q installation folder | See [here](https://code.kx.com/q/learn/install/#step-5-edit-your-profile) for more information. | -| `QLIC` | Path to the folder where the q license should be found | See [here](https://code.kx.com/q/learn/install/#step-5-edit-your-profile) for more information. | -| `QINIT` | Path to an additional `*.q` file loaded after KDB-X Python has initialized | See [here](https://code.kx.com/q4m3/14_Introduction_to_Kdb%2B/#1481-the-environment-variables) for more information. | -| `QCFG` | Location of KX config file. Defaults to `~/.kx/config` | | - -If no license is found, set the following variables either in configuration or as environment variables to define the `kc.lic` or `k4.lic` license used by KDB-X Python: - -| **Variable** | **Description** | -|---------------------|------------------------------------------------------------------------------------------------| -| `KDB_LICENSE_B64` | This should contain the base-64 encoded contents of a valid `kc.lic` file with `pykx` enabled. | -| `KDB_K4LICENSE_B64` | This should contain the base-64 encoded contents of a valid `k4.lic` file with `pykx` enabled. | - -#### KDB-X Python QARGS supported additions +Use these options to tune KDB-X Python behavior at run time. Set them before you import KDB-X Python. Each setting then stays in effect until the process ends. + +To turn on any option whose default is `False`, set it to `1` or `true`. + +### Examples + +These three routes are equivalent. Pick whichever suits how you start your application: + +=== "Configuration file" + + ```bash + [default] + PYKX_GC="true" + PYKX_RELEASE_GIL="true" + PYKX_MAX_ERROR_LENGTH="1024" + ``` + + Refer to [Configuration file](#configuration-file) for where KDB-X Python looks for this file. + +=== "Environment variables" + + ```python + >>> import os + >>> os.environ['PYKX_GC'] = '1' + >>> os.environ['PYKX_RELEASE_GIL'] = '1' + >>> os.environ['PYKX_MAX_ERROR_LENGTH'] = '1024' + >>> import pykx as kx + ``` + +=== "QARGS" + + ```sh + QARGS="--licensed --qce" python my_application.py + ``` + + `#!python QARGS` passes flags to the embedded q. Refer to [KDB-X Python QARGS supported additions](#kdb-x-python-qargs-supported-additions). + +### Licensing and startup + +| **Variable** | **Default** | **Description** | +| --- | --- | --- | +| `PYKX_LICENSED` | `False` | Always run in licensed mode. Refer to [Modes of operation](advanced/modes.md). | +| `PYKX_UNLICENSED` | `False` | Always run in unlicensed mode. Refer to [Modes of operation](advanced/modes.md). | +| `PYKX_BETA_FEATURES` | `False` | Enable all beta features, so you can test and prototype code slated for later releases. | +| `PYKX_NO_SIGNAL` | `False` | Leave Python [signal](https://docs.python.org/3/library/signal.html) definitions untouched. By default, KDB-X Python restores Python's definitions, which embedded q replaces when it initializes in licensed mode. | + +### q environment and libraries + +| **Variable** | **Default** | **Description** | +| --- | --- | --- | +| `PYKX_IGNORE_QHOME` | `False` | Stop KDB-X Python taking over the `QHOME` environment variable: skip symlinking your `QHOME` contents into its internal directory, and leave `QHOME` pointing at your original installation, for this process and any q subprocess. Matters most when you start q subprocesses (especially kdb+ 4.0/4.1) or work on Windows. Refer to [QHOME, symlinking, and subprocesses](#qhome-symlinking-and-subprocesses). | +| `PYKX_Q_LIB_LOCATION` | Unset | Directory holding the q libraries KDB-X Python loads. Set it to store those libraries separately from your Python installation, which some enterprise deployments require. The directory must mirror the `lib` directory of your installed `pykx` package: the `*.q` and `*.k` scripts at the top level, plus the platform subdirectory (`l64`, `l64arm`, `m64`, `m64arm`, or `w64`) holding the native libraries. Maintain it yourself so its contents match your KDB-X Python version. | +| `PYKX_QCE` | `False` | On Linux, load the q Cloud Edition features that ship with [Insights Core](https://code.kx.com/insights/core/). | +| `PYKX_Q_EXECUTABLE` | `q` | Path to the q executable that the [`tick`](../api/tick.md) module calls when building streaming infrastructures. | + +### Performance and concurrency + +| **Variable** | **Default** | **Description** | +| --- | --- | --- | +| `PYKX_NO_ALLOCATOR` | `False` | Copy NumPy array data in full instead of letting q allocate it. By default, KDB-X Python implements [NEP-49](https://numpy.org/neps/nep-0049.html) so that q handles NumPy memory allocation and converts arrays more efficiently, avoiding a copy where it can. | +| `PYKX_GC` | `False` | Trigger the q garbage collector when Python deallocates a NumPy array that KDB-X Python allocated. This returns q memory to the OS at the cost of a small overhead. Requires `PYKX_NO_ALLOCATOR` to be unset. | +| `PYKX_RELEASE_GIL` | `False` | Release the Python Global Interpreter Lock when calling into q. | +| `PYKX_Q_LOCK` | `False` | Add a re-entrant lock around calls into q, which stops two threads calling into q at once. This keeps embedded q thread safe even when you set `PYKX_RELEASE_GIL`. | +| `PYKX_THREADING` | `False` | Start embedded q in a background thread, so a call into q from any thread can modify state. Licensed mode only. | +| `PYKX_LOAD_PYARROW_UNSAFE` | `False` | Import PyArrow directly, which speeds up importing KDB-X Python slightly. By default, KDB-X Python imports PyArrow in a subprocess, because an incompatible PyArrow version can crash the process. | + +### Behavior and diagnostics + +| **Variable** | **Default** | **Description** | +| --- | --- | --- | +| `PYKX_KEEP_LOCAL_TIMES` | `False` | Convert Python datetime objects using the local time zone. By default, KDB-X Python translates them to UTC first. | +| `PYKX_QDEBUG` | `False` | Return backtrace information when a q function raises an error. Alternatively, pass `debug=True` to a `kx.q` call. | +| `PYKX_DEBUG_INSIGHTS_LIBRARIES` | `False` | Print the full error output when the Insights libraries fail to load. | +| `PYKX_MAX_ERROR_LENGTH` | `256` | Message buffer length, in characters, that KDB-X Python uses to report IPC connection errors. Lower it to stop long error messages polluting your logs. | +| `PYKX_SUPPRESS_WARNINGS` | `False` | Suppress the warnings KDB-X Python raises where an edge case can cause unexpected behavior. KDB-X Python still warns about behavior it explicitly does not support. | +| `PYKX_JUPYTERQ` | `False` | Start every Jupyter notebook in q-first mode when you import KDB-X Python. | + +### q environment variables + +These variables set the environment for the q that KDB-X Python embeds in licensed mode: + +| **Variable** | **Values** | **Description** | +| --- | --- | --- | +| `QARGS` | Command-line flags | Flags to pass to q. Refer to the [q command-line reference](https://code.kx.com/q/basics/cmdline/). | +| `QHOME` | Path to a directory | Your q installation folder. KDB-X Python manages this variable in licensed mode – refer to [QHOME, symlinking, and subprocesses](#qhome-symlinking-and-subprocesses). | +| `QLIC` | Path to a directory | The folder holding your q license. Refer to [Install a KDB-X license](../getting-started/installing.md#install-a-kdb-x-license). | +| `QINIT` | Path to a file | An extra `*.q` file that KDB-X Python loads once it finishes initializing. Refer to [Introduction to kdb+ environment variables](https://code.kx.com/q4m3/14_Introduction_to_Kdb%2B/#1481-the-environment-variables). | +| `QCFG` | Path to a file | Location of the KX config file. Defaults to `~/.kx/config`. | + +### License variables + +If KDB-X Python finds no license, set one of these variables to supply a base64-encoded license, either in configuration or as an environment variable. To obtain and install a license, refer to [Install a KDB-X license](../getting-started/installing.md#install-a-kdb-x-license); to renew or upgrade one, refer to [Manage your license](advanced/license.md). + +| **Variable** | **Values** | **Description** | +| --- | --- | --- | +| `KDB_LICENSE_B64` | Base64-encoded `kc.lic` contents | Supplies a `kc.lic` license with `pykx` enabled. | +| `KDB_K4LICENSE_B64` | Base64-encoded `k4.lic` contents | Supplies a `k4.lic` license with `pykx` enabled. | + +### KDB-X Python QARGS supported additions When using KDB-X Python, you can define `#!python QARGS` to modify its behavior during initialization in a Linux environment. Here are some of the values you can use for `#!python QARGS`: -| **Input** | **Description** | -|-------------|---------------------------------------------------------------------------------| -| `--qce` | Loads KDB-X libraries at initialization of KDB-X. | -| `--kurl` | Ensure that the KDB-X `kurl` library is not loaded at initialization. | -| `--objstor` | Ensure that the KDB-X `objstor` library is not loaded at initialization. | -| `--qlog` | Ensure that the KDB-X `qlog` library is not loaded at initialization. | -| `--sql` | Ensure that the KDB-X `sql` library is not loaded at initialization. | +| **Input** | **Description** | +|-------------|--------------------------------------------------------------------------| +| `--qce` | Loads all the KDB-X libraries below at initialization. | +| `--kurl` | Loads the KDB-X `kurl` library at initialization. | +| `--objstor` | Loads the KDB-X `objstor` library at initialization. | +| `--qlog` | Loads the KDB-X `qlog` library at initialization. | +| `--sql` | Loads the KDB-X `sql` library at initialization. | + +!!! note "These flags are opt-in" + + KDB-X Python loads a library only when you set its flag. Earlier versions loaded them all by default and used `--no-qce`, `--no-kurl`, `--no-objstor`, `--no-qlog`, and `--no-sql` to opt out; those flags no longer exist. Refer to [Migrating from PyKX 3.\* to KDB-X Python 4.\*](../upgrades/3040.md#qargs-flag-updates). ### KDB-X Python under q -You can load KDB-X Python and [use it from a q session](../pykx-under-q/intro.md). The following variables are specific to this mode of operation: +You can load KDB-X Python and [use it from a q session](../pykx-under-q/intro.md). These variables apply only to that mode: -| **Variable** | **Values** | **Description** | -|---------------------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `PYKX_DEFAULT_CONVERSION` | `py`, `np`, `pd`, `pa` or `k` | Default conversion to apply when passing q objects to Python. Converting to Numpy (`np`) by default. | -| `PYKX_SKIP_UNDERQ` | `1` or `true` | When importing KDB-X Python from Python, KDB-X Python also loads `pykx.q` under its embedded q. This variable skips this step. | -| `PYKX_EXECUTABLE` | File path | The path to use for the Python executable | -| `PYKX_USE_FIND_LIBPYTHON` | `1` or `true` | Should the Python package [`find-libpython`](https://pypi.org/project/find-libpython/) be used to determine the location of `libpython.[so|dll]`, this manually could be done by setting the location `PYKX_PYTHON_LIB_PATH` | -| `PYKX_PYTHON_LIB_PATH` | File path | The path to use for loading libpython. | -| `PYKX_PYTHON_BASE_PATH` | File path | The path to use for the base directory of your Python installation. | -| `PYKX_PYTHON_HOME_PATH` | File path | The path to use for the base Python home directory (used to find site packages). | +| **Variable** | **Values** | **Description** | +| --- | --- | --- | +| `PYKX_DEFAULT_CONVERSION` | `py`, `np`, `pd`, `pa`, or `k` | Conversion to apply when passing q objects to Python. Defaults to NumPy (`np`). | +| `PYKX_SKIP_UNDERQ` | `1` or `true` | Skip loading `pykx.q` under embedded q, which KDB-X Python otherwise does when you import it from Python. | +| `PYKX_EXECUTABLE` | File path | Path to the Python executable. | +| `PYKX_USE_FIND_LIBPYTHON` | `1` or `true` | Use [`find-libpython`](https://pypi.org/project/find-libpython/) to locate `libpython.so` or `libpython.dll`. To set that location yourself, use `PYKX_PYTHON_LIB_PATH` instead. | +| `PYKX_PYTHON_LIB_PATH` | File path | Path KDB-X Python loads `libpython` from. | +| `PYKX_PYTHON_BASE_PATH` | File path | Base directory of your Python installation. | +| `PYKX_PYTHON_HOME_PATH` | File path | Base Python home directory, which KDB-X Python uses to find site packages. | +!!! note "These variables no longer reach child processes" + + Loading KDB-X Python under q no longer exports `PYKX_SKIP_UNDERQ`, and `#!python .pykx.setdefault` no longer exports `PYKX_DEFAULT_CONVERSION`. A Python process started from that q session imports with the full `#!python .pykx` API and its own default conversion. Refer to the [KDB-X Python under q changelog](../release-notes/underq-changelog.md). ### q Cloud Edition features with Insights Core (Linux only) -On Linux, the q Cloud Edition features coming with Insights Core can be used to read data from Cloud Storage (AWS S3, Google Cloud Storage, Azure Blob Storage). Credentials to access the Cloud Storage can be passed using specific environment variables. For more information, go to: +On Linux, the q Cloud Edition features from Insights Core read data from cloud storage: AWS S3, Google Cloud Storage, and Azure Blob Storage. Load these features with `PYKX_QCE` or the [QARGS flags](#kdb-x-python-qargs-supported-additions), then pass your cloud storage credentials in environment variables. For more information, go to: - [kdb Insights SDK environment variables](https://code.kx.com/insights/core/objstor/main.html#environment-variables) - [kdb Insights SDK automatic registration using credential discovery](https://code.kx.com/insights/core/kurl/kurl.html#automatic-registration-using-credential-discovery) +## Related topics + +- [Compare the modes of operation](advanced/modes.md) +- [Use KDB-X Python in a Python subprocess](advanced/subprocess.md) +- [Install a KDB-X license](../getting-started/installing.md#install-a-kdb-x-license) +- [Manage your license](advanced/license.md) +- [Enable multithreading](advanced/threading.md) +- [Set up Real-Time Capture](advanced/streaming/index.md) +- [Review deprecated configuration options](../release-notes/deprecations.md) +- [Troubleshoot errors](../help/troubleshooting.md) + +## Next steps + +- [Run the quickstart](../getting-started/quickstart.md) +- [Explore KDB-X Python fundamentals](../examples/interface-overview.md) + diff --git a/docs/user-guide/fundamentals/creating.md b/docs/user-guide/fundamentals/creating.md index 23c78e2..803fa1f 100644 --- a/docs/user-guide/fundamentals/creating.md +++ b/docs/user-guide/fundamentals/creating.md @@ -52,7 +52,7 @@ The simplest way to create a `pykx` object is by converting a similar Python typ pykx.FloatVector(pykx.q('1 2 3f')) >>> kx.toq(pydict) pykx.Dictionary(pykx.q(' - x| (1;2;3) + x| 1 2 3 y| (,`x)!,3 ')) ``` @@ -90,7 +90,7 @@ The simplest way to create a `pykx` object is by converting a similar Python typ >>> df = pd.DataFrame.from_dict({'x': [1, 2], 'y': ['a', 'b']}) >>> kx.toq(pdseries1) pykx.LongVector(pykx.q('1 2 3')) - >>> kx.toq(pdseries1, kx.FloatVector) + >>> kx.toq(pd.Series([1, 2, 3], dtype=float)) pykx.FloatVector(pykx.q('1 2 3f')) >>> kx.toq(pdseries2) pykx.IntVector(pykx.q('1 2 3i')) @@ -103,14 +103,14 @@ The simplest way to create a `pykx` object is by converting a similar Python typ ')) >>> kx.toq(df).dtypes pykx.Table(pykx.q(' - columns type + columns datatypes ----------------------- x "kx.LongAtom" y "kx.SymbolAtom" ')) >>> kx.toq(df, ktype={'x': kx.FloatVector}).dtypes pykx.Table(pykx.q(' - columns type + columns datatypes ----------------------- x "kx.FloatAtom" y "kx.SymbolAtom" @@ -135,7 +135,7 @@ The simplest way to create a `pykx` object is by converting a similar Python typ 0n 1 ')) >>> kx.toq(dict_arr) - pykx.List(pykx.q(' + pykx.Table(pykx.q(' x y z -------- 1 1b :: @@ -157,14 +157,14 @@ The simplest way to create a `pykx` object is by converting a similar Python typ ')) >>> kx.toq(tab).dtypes pykx.Table(pykx.q(' - columns type + columns datatypes ----------------------- n_legs "kx.LongAtom" animals "kx.SymbolAtom" ')) >>> kx.toq(tab, {'animals': kx.CharVector}).dtypes pykx.Table(pykx.q(' - columns type + columns datatypes ----------------------- n_legs "kx.LongAtom" animals "kx.CharVector" diff --git a/docs/user-guide/fundamentals/evaluating.md b/docs/user-guide/fundamentals/evaluating.md index 8b19a4a..21c159b 100644 --- a/docs/user-guide/fundamentals/evaluating.md +++ b/docs/user-guide/fundamentals/evaluating.md @@ -164,7 +164,7 @@ For instance, you can apply the `#!python each` adverb to `#!python is_prime` an ```python >>> is_prime.each(range(10)) -pykx.LongVector(q('0 0 1 1 0 1 0 1 0 0')) +pykx.LongVector(pykx.q('0 0 1 1 0 1 0 1 0 0')) ``` Then you could pass that into [`pykx.q.where`](../../api/pykx-execution/q.md#where): @@ -177,13 +177,13 @@ pykx.LongVector(pykx.q('2 3 5 7')) Context is persisted between embedded calls to q, but not calls over IPC. ```python ->>> kx.q('\d .abc') # change to the `.abc` context +>>> kx.q(r'\d .abc') # change to the `.abc` context pykx.Identity(pykx.q('::')) >>> kx.q('xyz: 1 2 3') # set variable `xyz` within the `.abc` context pykx.Identity(pykx.q('::')) >>> kx.q('.abc.xyz') pykx.LongVector(pykx.q('1 2 3')) ->>> kx.q('\d .') # change back to the default `.` global context +>>> kx.q(r'\d .') # change back to the default `.` global context pykx.Identity(pykx.q('::')) >>> kx.q('xyz: 4 5 6') # set variable `xyz` within the `.` global context pykx.Identity(pykx.q('::')) @@ -192,7 +192,7 @@ pykx.LongVector(pykx.q('1 2 3')) >>> kx.q('xyz') pykx.LongVector(pykx.q('4 5 6')) >>> q = kx.QConnection('localhost', 5001) ->>> q('\d .abc') +>>> q(r'\d .abc') pykx.Identity(pykx.q('::')) >>> q('xyz: 1 2 3') pykx.Identity(pykx.q('::')) diff --git a/docs/user-guide/fundamentals/indexing.md b/docs/user-guide/fundamentals/indexing.md index 41ab55f..4f38354 100644 --- a/docs/user-guide/fundamentals/indexing.md +++ b/docs/user-guide/fundamentals/indexing.md @@ -198,6 +198,7 @@ In addition to positional assignment, you can use the `#!python append` and `#!p pykx.exceptions.QError: Appending data of type: to vector of type: not supported >>> >>> qvec.extend([1.2, 1.3, 1.4]) + >>> qvec pykx.FloatVector(pykx.q('0.7742128 0.7049724 0.5212126 1.1 1.2 1.3 1.4')) >>> >>> qvec.extend([1, 2, 3]) diff --git a/docs/user-guide/fundamentals/nulls_and_infinities.md b/docs/user-guide/fundamentals/nulls_and_infinities.md index 1b709e1..40a1981 100644 --- a/docs/user-guide/fundamentals/nulls_and_infinities.md +++ b/docs/user-guide/fundamentals/nulls_and_infinities.md @@ -90,7 +90,7 @@ If you apply [the q function named null](https://code.kx.com/q/ref/null/) to mos >>> import pykx as kx >>> kx.q.null(kx.q('0n')) pykx.BooleanAtom(pykx.q('1b')) ->>> kx.q.null(('1 2 0n 3f')) +>>> kx.q.null(kx.q('1 2 0n 3f')) pykx.BooleanVector(pykx.q('0010b')) ``` @@ -130,104 +130,100 @@ False ``` Some null values are unintuitive. For instance, the null value for a character in q is the space `#!python " "`, the null value for a symbol is the empty symbol, and the null value for a GUID is `#!python 00000000-0000-0000-0000-000000000000`. A char vector (i.e. q string) that has any spaces has `#!python has_nulls` set to `#!python True`. -See also the page with specifics on [temporal](./temporal.md) conversions. +Refer to [temporal conversions](./temporal.md) for details about temporal null and infinity values. ## q to Python -### Null conversions +`.py()`, `.np()`, `.pd()`, and `.pa()` convert q atoms and vectors to different Python representations. Select a tab in each section to compare their results. -!!! note "Note" +### Null conversions - KDB-X Python null conversion behavior changed in version 3.0.0. The below table outlines the before and after conversions. - - === ".py()" - - | datatype | q value | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|---------|---------------------|-----------------------|---------------------|-----------------------| - | guid | `0Ng` | `UUID(int=0)` | `UUID(int=0)` | | | - | short | `0Nh` | `q('0Nh')` | `q('0Nh')` | `pd.NA` | `pd.NA` | - | int | `0Ni` | `q('0Ni')` | `q('0Ni')` | `pd.NA` | `pd.NA` | - | long | `0Nj` | `q('0N')` | `q('0N')` | `pd.NA` | `pd.NA` | - | real | `0Ne` | `float('nan')` | `float('nan')` | | | - | float | `0n` | `float('nan')` | `float('nan')` | | | - | character | `" "` | `b' '` | `b' '` | | | - | symbol | `` ` `` | `''` | `''` | | | - | timestamp | `0Np` | `None` | `q('0Np')` | `pd.NaT` | `pd.NaT` | - | month | `0Nm` | `None` | `q('0Nm')` | `pd.NaT` | `pd.NaT` | - | date | `0Nd` | `None` | `q('0Nd')` | `pd.NaT` | `pd.NaT` | - | timespan | `0Nn` | `pd.NaT` | `q('0Nn')` | | `pd.NaT` | - | minute | `0Nu` | `pd.NaT` | `q('0Nu')` | | `pd.NaT` | - | second | `0Nv` | `pd.NaT` | `q('0Nv')` | | `pd.NaT` | - | time | `0Nt` | `pd.NaT` | `q('0Nt')` | | `pd.NaT` | - - === ".np()" - - | datatype | q value | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.vector conversion | - |-----------|---------|-------------------------|----------------------------------|----------------------------------|---------------------| - | guid | `0Ng` | `UUID(int=0)` | `UUID(int=0)` | | | - | short | `0Nh` | **1 | `np.int16(-32768)` | `np.int16(-32768)` | | - | int | `0Ni` | **1 | `np.int32(-2147483648)` | `np.int32(-2147483648)` | | - | long | `0Nj` | **1 | `np.int64(-9223372036854775808)` | `np.int64(-9223372036854775808)` | | - | real | `0Ng` | `np.float32('nan')` | `np.float32('nan')` | | | - | float | `0n` | `np.float64('nan')` | `np.float64('nan')` | | | - | character | `" "` | `b' '` | `np.bytes_(' ')` | | | - | symbol | `` ` `` | `''` | `''` | | | - | timestamp | `0Np` | `np.datetime64('NaT')` | `np.datetime64('NaT')` | | | - | month | `0Nm` | `np.datetime64('NaT')` | `np.datetime64('NaT')` | | | - | date | `0Nd` | `np.datetime64('NaT')` | `np.datetime64('NaT')` | | | - | timespan | `0Nn` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | | | - | minute | `0Nu` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | | | - | second | `0Nv` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | | | - | time | `0Nt` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | | | - - - **1 Errors: `NumPy does not support null atomic integral values for short int long` - - === ".pd()" - - | datatype | q value | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|---------|---------------------|-----------------------|---------------------|-----------------------| - | guid | `0Ng` | `UUID(int=0)` | `UUID(int=0)` | | | - | short | `0Nh` | **1 | `pd.NA` | `pd.NA` | | - | int | `0Ni` | **1 | `pd.NA` | `pd.NA` | | - | long | `0Nj` | **1 | `pd.NA` | `pd.NA` | | - | real | `0Ne` | `np.float32('nan')` | `np.float32('nan')` | | | - | float | `0n` | `np.float64('nan')` | `np.float64('nan')` | | | - | character | `" "` | `b' '` | `np.bytes_(' ')` | | | - | symbol | `` ` `` | `''` | `''` | | | - | timestamp | `0Np` | `pd.NaT` | `pd.NaT` | | | - | month | `0Nm` | `pd.NaT` | `pd.NaT` | | | - | date | `0Nd` | `pd.NaT` | `pd.NaT` | | | - | timespan | `0Nn` | `pd.NaT` | `pd.NaT` | | | - | minute | `0Nu` | `pd.NaT` | `pd.NaT` | | | - | second | `0Nv` | `pd.NaT` | `pd.NaT` | | | - | time | `0Nt` | `pd.NaT` | `pd.NaT` | | | - - - **1 Errors: `NumPy does not support null atomic integral values for short int long` - - === ".pa()" - - | datatype | q value | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|---------|---------------------|-------------------------------------------------------|---------------------|-----------------------| - | guid | `0Ng` | `UUID(int=0)` | **1 | | | - | short | `0Nh` | **2 | **2 | `pd.NA` | | - | int | `0Ni` | **2 | **2 | `pd.NA` | | - | long | `0Nj` | **2 | **2 | `pd.NA` | | - | real | `0Ne` | `np.float32('nan')` | `pa.array([np.float32('nan')], type=pa.float32())[0]` | | | - | float | `0n` | `np.float64('nan')` | `pa.array([np.float32('nan')], type=pa.float64())[0]` | | | - | character | `" "` | `b' '` | `pa.array([b' '], pa.binary())[0]` | | | - | symbol | `` ` `` | `''` | `pa.array([''], pa.string())[0]` | | | - | timestamp | `0Np` | `pd.NaT` | **3 | | | - | month | `0Nm` | `pd.NaT` | **3 | | | - | date | `0Nd` | `pd.NaT` | **3 | | | - | timespan | `0Nn` | `pd.NaT` | **4 | | | - | minute | `0Nu` | `pd.NaT` | **4 | | | - | second | `0Nv` | `pd.NaT` | **4 | | | - | time | `0Nt` | `pd.NaT` | **4 | | | - - - **1 Errors: `Could not convert UUID('00000000-0000-0000-0000-000000000000') with type UUID: did not recognize Python value type when inferring an Arrow data type` - - **2 Errors: `NumPy does not support null atomic integral values for short int long` - - **3 Errors: `pyarrow.lib.ArrowNotImplementedError: Unbound or generic datetime64 time unit` - - **4 Errors: `pyarrow.lib.ArrowNotImplementedError: Unbound or generic timedelta64 time unit` +These tables show how each method converts q null atoms and vectors. + +=== ".py()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|---------|-----------------|-------------------| + | guid | `0Ng` | `UUID(int=0)` | `UUID(int=0)` | + | short | `0Nh` | `pd.NA` | `pd.NA` | + | int | `0Ni` | `pd.NA` | `pd.NA` | + | long | `0Nj` | `pd.NA` | `pd.NA` | + | real | `0Ne` | `float('nan')` | `float('nan')` | + | float | `0n` | `float('nan')` | `float('nan')` | + | character | `" "` | `b' '` | `b' '` | + | symbol | `` ` `` | `''` | `''` | + | timestamp | `0Np` | `pd.NaT` | `pd.NaT` | + | month | `0Nm` | `pd.NaT` | `pd.NaT` | + | date | `0Nd` | `pd.NaT` | `pd.NaT` | + | timespan | `0Nn` | `pd.NaT` | `pd.NaT` | + | minute | `0Nu` | `pd.NaT` | `pd.NaT` | + | second | `0Nv` | `pd.NaT` | `pd.NaT` | + | time | `0Nt` | `pd.NaT` | `pd.NaT` | + +=== ".np()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|---------|----------------------------------|----------------------------------| + | guid | `0Ng` | `UUID(int=0)` | `UUID(int=0)` | + | short | `0Nh` | `np.int16(-32768)` | `np.int16(-32768)` | + | int | `0Ni` | `np.int32(-2147483648)` | `np.int32(-2147483648)` | + | long | `0Nj` | `np.int64(-9223372036854775808)` | `np.int64(-9223372036854775808)` | + | real | `0Ne` | `np.float32('nan')` | `np.float32('nan')` | + | float | `0n` | `np.float64('nan')` | `np.float64('nan')` | + | character | `" "` | `b' '` | `np.bytes_(' ')` | + | symbol | `` ` `` | `''` | `''` | + | timestamp | `0Np` | `np.datetime64('NaT')` | `np.datetime64('NaT')` | + | month | `0Nm` | `np.datetime64('NaT')` | `np.datetime64('NaT')` | + | date | `0Nd` | `np.datetime64('NaT')` | `np.datetime64('NaT')` | + | timespan | `0Nn` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | + | minute | `0Nu` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | + | second | `0Nv` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | + | time | `0Nt` | `np.timedelta64('NaT')` | `np.timedelta64('NaT')` | + +=== ".pd()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|---------|---------------------|---------------------| + | guid | `0Ng` | `UUID(int=0)` | `UUID(int=0)` | + | short | `0Nh` | `pd.NA` | `pd.NA` | + | int | `0Ni` | `pd.NA` | `pd.NA` | + | long | `0Nj` | `pd.NA` | `pd.NA` | + | real | `0Ne` | `np.float32('nan')` | `np.float32('nan')` | + | float | `0n` | `np.float64('nan')` | `np.float64('nan')` | + | character | `" "` | `b' '` | `np.bytes_(' ')` | + | symbol | `` ` `` | `''` | `''` | + | timestamp | `0Np` | `pd.NaT` | `pd.NaT` | + | month | `0Nm` | `pd.NaT` | `pd.NaT` | + | date | `0Nd` | `pd.NaT` | `pd.NaT` | + | timespan | `0Nn` | `pd.NaT` | `pd.NaT` | + | minute | `0Nu` | `pd.NaT` | `pd.NaT` | + | second | `0Nv` | `pd.NaT` | `pd.NaT` | + | time | `0Nt` | `pd.NaT` | `pd.NaT` | + +=== ".pa()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|---------|---------------------|-------------------------------------------------------| + | guid | `0Ng` | `UUID(int=0)` | `` | + | short | `0Nh` | `pd.NA` | `` | + | int | `0Ni` | `pd.NA` | `` | + | long | `0Nj` | `pd.NA` | `` | + | real | `0Ne` | `np.float32('nan')` | `pa.array([np.float32('nan')], type=pa.float32())[0]` | + | float | `0n` | `np.float64('nan')` | `pa.array([np.float32('nan')], type=pa.float64())[0]` | + | character | `" "` | `b' '` | `pa.array([b' '], pa.binary())[0]` | + | symbol | `` ` `` | `''` | `pa.array([''], pa.string())[0]` | + | timestamp | `0Np` | `pd.NaT` | `` | + | month | `0Nm` | `pd.NaT` | **Error [1]** | + | date | `0Nd` | `pd.NaT` | `` | + | timespan | `0Nn` | `pd.NaT` | `` | + | minute | `0Nu` | `pd.NaT` | **Error [2]** | + | second | `0Nv` | `pd.NaT` | `` | + | time | `0Nt` | `pd.NaT` | `` | + + !!! warning "Conversion errors" + + - **[1]** PyArrow raises `pyarrow.lib.ArrowNotImplementedError: Unsupported datetime64 time unit`. + - **[2]** PyArrow raises `pyarrow.lib.ArrowNotImplementedError: Unsupported timedelta64 time unit`. To convert vectors with the q types `#!python short`, `#!python int`, and `#!python long` to Python, you can use the following methods: @@ -394,7 +390,7 @@ While `#!python -9223372036854778080` represents an underlying KDB-X Python Null >>> import pandas as pd >>> t = kx.q('([] time:.z.p;a:til 1000;b:9,999#0N)') >>> pd.set_option('display.max_rows', 1000) ->>> t.pd +>>> t.pd() time a b 0 2023-11-26 22:16:05.885992 0 9 1 2023-11-26 22:16:05.885992 1 -- @@ -411,175 +407,172 @@ While `#!python -9223372036854778080` represents an underlying KDB-X Python Null - [Pandas working with missing data](https://pandas.pydata.org/docs/user_guide/missing_data.html) - [Pandas nullable integer data types](https://pandas.pydata.org/docs/user_guide/integer_na.html#integer-na) -### Infinite Conversions - -See also the page with specifics on [temporal](./temporal.md) conversions to explain further some of the difficulties around infinities while converting. - -!!! note "Note" - - KDB-X Python infinite conversion behavior changed in version 3.0.0. The below tables outline the before and after conversions. - - #### Positive Infinity conversions - - === ".py()" - - | datatype | q value | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|---------|------------------------------------------------------|-----------------------------------------------------|------------------------------------------------------|------------------------------------------------------| - | short | `0Wh` | `q('0Wh')` | `q('0Wh')` | `float('inf')` | `float('inf')` | - | int | `0Wi` | `q('0Wi')` | `q('0Wi')` | `float('inf')` | `float('inf')` | - | long | `0Wj` | `q('0W')` | `q('0W')` | `float('inf')` | `float('inf')` | - | real | `0We` | `float('inf')` | `float('inf')` | | | - | float | `0w` | `float('inf')` | `float('inf')` | | | - | timestamp | `0Wp` | `datetime.datetime(2262, 4, 11, 23, 47, 16, 854775)` | `datetime.datetime(1707, 9, 22, 0, 12, 43, 145224)` | | | - | month | `0Wm` | `2147484007` | `2147484007` | | | - | date | `0Wd` | `2147494604` | `2147494604` | | | - | timespan | `0Wn` | `datetime.timedelta(106751, 16, 854775, 0, 47, 23)` | `datetime.timedelta(106751, 16, 854775, 0, 47, 23)` | | | - | minute | `0Wu` | `datetime.timedelta(-3220, 4, 33138, 0, 5, 5)` | `datetime.timedelta(-3220, 4, 33138, 0, 5, 5)` | | | - | second | `0Wv` | `datetime.timedelta(24855, 7, 0, 0, 14, 3)` | `datetime.timedelta(24855, 7, 0, 0, 14, 3)` | | | - | time | `0Wt` | `datetime.timedelta(24, 23, 647000, 0, 31, 20)` | `datetime.timedelta(24, 23, 647000, 0, 31, 20)` | | | - - === ".np()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|-------|--------------------------------------------------|--------------------------------------------------|---------------------------------|--------------------------------------------------| - | short | `0Wh` | **1 | `np.int16(32767)` | `np.int16(32767)` | | - | int | `0Wi` | **1 | `np.int32(2147483647)` | `np.int32(2147483647)` | | - | long | `0Wj` | **1 | `np.int64(9223372036854775807)` | `np.int64(9223372036854775807)` | | - | real | `0We` | `np.float32('inf')` | `np.float32('inf')` | | | - | float | `0w` | `np.float64('inf')` | `np.float64('inf')` | | | - | timestamp | `0Wp` | `np.datetime64('2262-04-11T23:47:16.854775807')` | `np.datetime64('1707-09-22T00:12:43.145224191')` | | | - | month | `0Wm` | `np.datetime64('178958970-08')` | `np.datetime64('-178954971-04')` | | | - | date | `0Wd` | `np.datetime64('5881610-07-11')` | `np.datetime64('-5877611-06-21')` | | | - | timespan | `0Wn` | `np.timedelta64(9223372036854775807, 'ns')` | `np.timedelta64(9223372036854775807, 'ns')` | | | - | minute | `0Wu` | `np.timedelta64(2147483647, 'm')` | `np.timedelta64(2147483647, 'm')` | | | - | second | `0Wv` | `np.timedelta64(2147483647, 's')` | `np.timedelta64(2147483647, 's')` | | | - | time | `0Wt` | `np.timedelta64(2147483647, 'ms')` | `np.timedelta64(2147483647, 'ms')` | | | - - - **1 Errors: `NumPy does not support infinite atomic integral values` - - === "pd()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|-------|-------------------------------------------------|-------------------------------------------------|---------------------------------|-------------------------------------------------| - | short | `0Wh` | **1 | `np.int16(32767)` | `np.int16(32767)` | | - | int | `0Wi` | **1 | `np.int32(2147483647)` | `np.int32(2147483647)` | | - | long | `0Wj` | **1 | `np.int64(9223372036854775807)` | `np.int64(9223372036854775807)` | | - | real | `0We` | `np.float32('inf')` | `np.float32('inf')` | | | - | float | `0w` | `np.float64('inf')` | `np.float64('inf')` | | | - | timestamp | `0Wp` | `pd.Timestamp('2262-04-11T23:47:16.854775807')` | `pd.Timestamp('1707-09-22T00:12:43.145224191')` | | | - | month | `0Wm` | **2 `Timestamp('178958970-08-01 00:00:00')` | **2 `Timestamp('178958970-08-01 00:00:00')` | | | - | date | `0Wd` | **2 `Timestamp('5881610-07-11 00:00:00')` | **2 `Timestamp('5881610-07-11 00:00:00')` | | | - | timespan | `0Wn` | `pd.Timedelta(9223372036854775807, 'ns')` | `pd.Timedelta(9223372036854775807, 'ns')` | | | - | minute | `0Wu` | **2 `Timedelta('1491308 days 02:07:00')` | **2 `Timedelta('1491308 days 02:07:00')` | | | - | second | `0Wv` | `pd.Timedelta(2147483647, 's')` | `pd.Timedelta(2147483647, 's')` | | | - | time | `0Wt` | `pd.Timedelta(2147483647, 'ms')` | `pd.Timedelta(2147483647, 'ms')` | | | - - - **1 Errors: `NumPy does not support infinite atomic integral values - - **2 Errors: `Values out of range` Pandas constructors block creation of these values - - === ".pa()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|-------|-------------------------------------------------|---------------------------------------------------------------------------------------------|---------------------------------|--------------------------------------------------------------| - | short | `0Wh` | **1 | `` | `np.int16(32767)` | | - | int | `0Wi` | **1 | `` | `np.int32(2147483647)` | | - | long | `0Wj` | **1 | `` | `np.int64(9223372036854775807)` | | - | real | `0We` | `np.float32('inf')` | `` | | | - | float | `0w` | `np.float64('inf')` | `` | | | - | timestamp | `0Wp` | `pd.Timestamp('2262-04-11T23:47:16.854775807')` | `` | | | - | month | `0Wm` | **2 `Timestamp('178958970-08-01 00:00:00')` | **3 | | | - | date | `0Wd` | **2 `Timestamp('5881610-07-11 00:00:00')` | **4 | | | - | timespan | `0Wn` | `pd.Timedelta(9223372036854775807, 'ns')` | `` | | | - | minute | `0Wu` | **2 `Timedelta('1491308 days 02:07:00')` | **5 | | | - | second | `0Wv` | `pd.Timedelta(2147483647, 's')` | `` | | | - | time | `0Wt` | `pd.Timedelta(2147483647, 'ms')` | `` | | | - - - **1 Errors: `NumPy does not support infinite atomic integral values` - - **2 Errors: `Values out of range - Pandas constructors block them` - - **3 Errors: `pyarrow.lib.ArrowNotImplementedError: Unsupported datetime64 time unit` - - **4 Errors: `OverflowError: days=-2147472692; must have magnitude <= 999999999` - - **5 Errors: `pyarrow.lib.ArrowNotImplementedError: Unsupported timedelta64 time unit` - - #### Negative Infinity conversions - - === ".py()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|--------|------------------------------------------------------|-----------------------------------------------------|-----------------------------------------------------|-----------------------| - | short | `-0Wh` | `q('-0Wh')` | `q('-0Wh')` | `float('-inf')` | `float('-inf')` | - | int | `-0Wi` | `q('-0Wi')` | `q('-0Wi')` | `float('-inf')` | `float('-inf')` | - | long | `-0Wj` | `q('-0W')` | `q('-0W')` | `float('-inf')` | `float('-inf')` | - | real | `-0We` | `float('-inf')` | `float('-inf')` | | | - | float | `-0w` | `float('-inf')` | `float('-inf')` | | | - | timestamp | `-0Wp` | `datetime.datetime(2262, 4, 11, 23, 47, 16, 854774)` | `datetime.datetime(1707, 9, 22, 0, 12, 43, 145224)` | `datetime.datetime(1707, 9, 22, 0, 12, 43, 145224)` | | - | month | `-0Wm` | `-2147483287` | `-2147483287` | | | - | date | `-0Wd` | `-2147472690` | `-2147472690` | | | - | timespan | `-0Wn` | `datetime.timedelta(-106752, 43, 145224, 0, 12)` | `datetime.timedelta(-106752, 43, 145224, 0, 12)` | | | - | minute | `-0Wu` | `datetime.timedelta(3219, 55, 966861, 0, 54, 18)` | `datetime.timedelta(3219, 55, 966861, 0, 54, 18)` | | | - | second | `-0Wv` | `datetime.timedelta(-24856, 53, 0, 0, 45, 20)` | `datetime.timedelta(-24856, 53, 0, 0, 45, 20)` | | | - | time | `-0Wt` | `datetime.timedelta(-25, 36, 353000, 0, 28, 3)` | `datetime.timedelta(-25, 36, 353000, 0, 28, 3)` | | | - - === "np()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|--------|--------------------------------------------------|--------------------------------------------------|--------------------------------------------------|-----------------------| - | short | `-0Wh` | **1 | `np.int16(-32767)` | `np.int16(-32767)` | | - | int | `-0Wi` | **1 | `np.int32(-2147483647)` | `np.int32(-2147483647)` | | - | long | `-0Wj` | **1 | `np.int64(-9223372036854775807)` | `np.int64(-9223372036854775807)` | | - | real | `-0We` | `np.float32('-inf')` | `np.float32('-inf')` | | | - | float | `-0w` | `np.float64('-inf')` | `np.float64('-inf')` | | | - | timestamp | `-0Wp` | `np.datetime64('1677-09-21T00:12:43.145224193')` | `np.datetime64('1707-09-22T00:12:43.145224193')` | `np.datetime64('1707-09-22T00:12:43.145224193')` | | - | month | `-0Wm` | `np.datetime64('-178954971-06')` | `np.datetime64('-178954971-06')` | | | - | date | `-0Wd` | `np.datetime64('-5877611-06-23')` | `np.datetime64('-5877611-06-23')` | | | - | timespan | `-0Wn` | `np.timedelta64(-9223372036854775807, 'ns')` | `np.timedelta64(-9223372036854775807, 'ns')` | | | - | minute | `-0Wu` | `np.timedelta64(-2147483647, 'm')` | `np.timedelta64(-2147483647, 'm')` | | | - | second | `-0Wv` | `np.timedelta64(-2147483647, 's')` | `np.timedelta64(-2147483647, 's')` | | | - | time | `-0Wt` | `np.timedelta64(-2147483647, 'ms')` | `np.timedelta64(-2147483647, 'ms')` | | | - - - **1 Errors: `NumPy does not support infinite atomic integral values` - - === ".pd()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|--------|-------------------------------------------------|-------------------------------------------------|-------------------------------------------------|-----------------------| - | short | `-0Wh` | **1 | `np.int16(-32767)` | `np.int16(-32767)` | | - | int | `-0Wi` | **1 | `np.int32(-2147483647)` | `np.int32(-2147483647)` | | - | long | `-0Wj` | **1 | `np.int64(-9223372036854775807)` | `np.int64(-9223372036854775807)` | | - | real | `-0We` | `np.float32('-inf')` | `np.float32('-inf')` | | | - | float | `-0w` | `np.float64('-inf')` | `np.float64('-inf')` | | | - | timestamp | `-0Wp` | `pd.Timestamp('1677-09-21T00:12:43.145224193')` | `pd.Timestamp('1707-09-22 00:12:43.145224193')` | `pd.Timestamp('1707-09-22 00:12:43.145224193')` | | - | month | `-0Wm` | **2 `Timestamp('-178954971-06-01 00:00:00')` | **2 `Timestamp('-178954971-06-01 00:00:00')` | | | - | date | `-0Wd` | **2 `Timestamp('-5877611-06-23 00:00:00')` | **2 `Timestamp('-5877611-06-23 00:00:00')` | | | - | timespan | `-0Wn` | `pd.Timedelta(-9223372036854775807, 'ns')` | `pd.Timedelta(-9223372036854775807, 'ns')` | | | - | minute | `-0Wu` | **2 `Timedelta('-1491309 days +21:53:00')` | **2 `Timedelta('-1491309 days +21:53:00')` | | | - | second | `-0Wv` | `pd.Timedelta(-2147483647, 's')` | `pd.Timedelta(-2147483647, 's')` | | | - | time | `-0Wt` | `pd.Timedelta(-2147483647, 'ms')` | `pd.Timedelta(-2147483647, 'ms')` | | | - - - **1 Errors: `NumPy does not support infinite atomic integral values` - - **2 Errors: `Values out of range` Pandas constructors block creation of these values - - === ".pa()" - - | datatype | | 2.* atom conversion | 2.* vector conversion | 3.* atom conversion | 3.* vector conversion | - |-----------|--------|-------------------------------------------------|----------------------------------------------------------------------------------------------|-------------------------------------------------|-----------------------| - | short | `-0Wh` | **1 | `` | `np.int16(-32767)` | | - | int | `-0Wi` | **1 | `` | `np.int32(-2147483647)` | | - | long | `-0Wj` | **1 | `` | `np.int64(-9223372036854775807)` | | - | real | `-0We` | `np.float32('-inf')` | `` | | | - | float | `-0w` | `np.float64('-inf')` | `` | | | - | timestamp | `-0Wp` | `pd.Timestamp('1677-09-21T00:12:43.145224193')` | `` | `pd.Timestamp('1707-09-22 00:12:43.145224193')` | | - | month | `-0Wm` | **2 `Timestamp('-178954971-06-01 00:00:00')` | **3 | | | - | date | `-0Wd` | **2 `Timestamp('-5877611-06-23 00:00:00')` | **4 | | | - | timespan | `-0Wn` | `pd.Timedelta(-9223372036854775807, 'ns')` | `` | | | - | minute | `-0Wu` | **2 `Timedelta('-1491309 days +21:53:00')` | **5 | | | - | second | `-0Wv` | `pd.Timedelta(-2147483647, 's')` | `` | | | - | time | `-0Wt` | `pd.Timedelta(-2147483647, 'ms')` | `` | | | - - - **1 Errors: `NumPy does not support infinite atomic integral values` - - **2 Errors: `Values out of range - Pandas constructors block them` - - **3 Errors: `pyarrow.lib.ArrowNotImplementedError: Unsupported datetime64 time unit` - - **4 Errors: `OverflowError: days=-2147472690; must have magnitude <= 999999999` - - **5 Errors: `pyarrow.lib.ArrowNotImplementedError: Unsupported timedelta64 time unit` +### Infinite conversions + +Python libraries cannot represent every typed q infinity directly, so results can differ by method, data type, and object shape. Refer to [temporal conversions](./temporal.md) for details about infinity conversions for temporal types. + + +#### Positive infinity conversions + +=== ".py()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|---------|------------------------------------------------------|------------------------------------------------------| + | short | `0Wh` | `float('inf')` | `float('inf')` | + | int | `0Wi` | `float('inf')` | `float('inf')` | + | long | `0Wj` | `float('inf')` | `float('inf')` | + | real | `0We` | `float('inf')` | `float('inf')` | + | float | `0w` | `float('inf')` | `float('inf')` | + | timestamp | `0Wp` | `datetime.datetime(2262, 4, 11, 23, 47, 16, 854775)` | `datetime.datetime(1707, 9, 22, 0, 12, 43, 145224)` | + | month | `0Wm` | `2147484007` | `2147484007` | + | date | `0Wd` | `2147494604` | `2147494604` | + | timespan | `0Wn` | `datetime.timedelta(106751, 16, 854775, 0, 47, 23)` | `datetime.timedelta(106751, 16, 854775, 0, 47, 23)` | + | minute | `0Wu` | `datetime.timedelta(days=1491308, seconds=7620)` | `datetime.timedelta(days=1491308, seconds=7620)` | + | second | `0Wv` | `datetime.timedelta(24855, 7, 0, 0, 14, 3)` | `datetime.timedelta(24855, 7, 0, 0, 14, 3)` | + | time | `0Wt` | `datetime.timedelta(24, 23, 647000, 0, 31, 20)` | `datetime.timedelta(24, 23, 647000, 0, 31, 20)` | + +=== ".np()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|-------|--------------------------------------------------|--------------------------------------------------| + | short | `0Wh` | `np.int16(32767)` | `np.int16(32767)` | + | int | `0Wi` | `np.int32(2147483647)` | `np.int32(2147483647)` | + | long | `0Wj` | `np.int64(9223372036854775807)` | `np.int64(9223372036854775807)` | + | real | `0We` | `np.float32('inf')` | `np.float32('inf')` | + | float | `0w` | `np.float64('inf')` | `np.float64('inf')` | + | timestamp | `0Wp` | `np.datetime64('2262-04-11T23:47:16.854775807')` | `np.datetime64('1707-09-22T00:12:43.145224191')` | + | month | `0Wm` | `np.datetime64('178958970-08')` | `np.datetime64('-178954971-04')` | + | date | `0Wd` | `np.datetime64('5881610-07-11')` | `np.datetime64('-5877611-06-21')` | + | timespan | `0Wn` | `np.timedelta64(9223372036854775807, 'ns')` | `np.timedelta64(9223372036854775807, 'ns')` | + | minute | `0Wu` | `np.timedelta64(2147483647, 'm')` | `np.timedelta64(2147483647, 'm')` | + | second | `0Wv` | `np.timedelta64(2147483647, 's')` | `np.timedelta64(2147483647, 's')` | + | time | `0Wt` | `np.timedelta64(2147483647, 'ms')` | `np.timedelta64(2147483647, 'ms')` | + +=== ".pd()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|-------|-------------------------------------------------|-------------------------------------------------| + | short | `0Wh` | `np.int16(32767)` | `np.int16(32767)` | + | int | `0Wi` | `np.int32(2147483647)` | `np.int32(2147483647)` | + | long | `0Wj` | `np.int64(9223372036854775807)` | `np.int64(9223372036854775807)` | + | real | `0We` | `np.float32('inf')` | `np.float32('inf')` | + | float | `0w` | `np.float64('inf')` | `np.float64('inf')` | + | timestamp | `0Wp` | `pd.Timestamp('2262-04-11T23:47:16.854775807')` | `pd.Timestamp('1707-09-22T00:12:43.145224191')` | + | month | `0Wm` | `Timestamp('178958970-08-01 00:00:00')` **[1]** | `Timestamp('-178954971-04-01 00:00:00')` **[1]** | + | date | `0Wd` | `Timestamp('5881610-07-11 00:00:00')` **[1]** | `Timestamp('-5877611-06-21 00:00:00')` **[1]** | + | timespan | `0Wn` | `pd.Timedelta(9223372036854775807, 'ns')` | `pd.Timedelta(9223372036854775807, 'ns')` | + | minute | `0Wu` | `Timedelta('1491308 days 02:07:00')` **[1]** | `Timedelta('1491308 days 02:07:00')` **[1]** | + | second | `0Wv` | `pd.Timedelta(2147483647, 's')` | `pd.Timedelta(2147483647, 's')` | + | time | `0Wt` | `pd.Timedelta(2147483647, 'ms')` | `pd.Timedelta(2147483647, 'ms')` | + + !!! warning "Pandas version requirement" + + - **[1]** These conversions require Pandas 2 or later. Pandas 1 raises `Values out of range` because its constructors cannot create these values. + +=== ".pa()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|-------|-------------------------------------------------|---------------------------------------------------------------------------------------------| + | short | `0Wh` | `np.int16(32767)` | `` | + | int | `0Wi` | `np.int32(2147483647)` | `` | + | long | `0Wj` | `np.int64(9223372036854775807)` | `` | + | real | `0We` | `np.float32('inf')` | `` | + | float | `0w` | `np.float64('inf')` | `` | + | timestamp | `0Wp` | `pd.Timestamp('2262-04-11T23:47:16.854775807')` | `` | + | month | `0Wm` | `Timestamp('178958970-08-01 00:00:00')` **[1]** | **Error [2]** | + | date | `0Wd` | `Timestamp('5881610-07-11 00:00:00')` **[1]** | **Error [3]** | + | timespan | `0Wn` | `pd.Timedelta(9223372036854775807, 'ns')` | `` | + | minute | `0Wu` | `Timedelta('1491308 days 02:07:00')` **[1]** | **Error [4]** | + | second | `0Wv` | `pd.Timedelta(2147483647, 's')` | `` | + | time | `0Wt` | `pd.Timedelta(2147483647, 'ms')` | `` | + + !!! warning "Conversion limitations" + + - **[1]** These conversions require Pandas 2 or later. Pandas 1 raises `Values out of range` because its constructors cannot create these values. + - **[2]** PyArrow raises `pyarrow.lib.ArrowNotImplementedError: Unsupported datetime64 time unit`. + - **[3]** The conversion raises `OverflowError: days=-2147472692; must have magnitude <= 999999999`. + - **[4]** PyArrow raises `pyarrow.lib.ArrowNotImplementedError: Unsupported timedelta64 time unit`. + +#### Negative infinity conversions + +=== ".py()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|--------|--------------------------------------------------------|--------------------------------------------------------| + | short | `-0Wh` | `float('-inf')` | `float('-inf')` | + | int | `-0Wi` | `float('-inf')` | `float('-inf')` | + | long | `-0Wj` | `float('-inf')` | `float('-inf')` | + | real | `-0We` | `float('-inf')` | `float('-inf')` | + | float | `-0w` | `float('-inf')` | `float('-inf')` | + | timestamp | `-0Wp` | `datetime.datetime(1707, 9, 22, 0, 12, 43, 145224)` | `datetime.datetime(1707, 9, 22, 0, 12, 43, 145224)` | + | month | `-0Wm` | `-2147483287` | `-2147483287` | + | date | `-0Wd` | `-2147472690` | `-2147472690` | + | timespan | `-0Wn` | `datetime.timedelta(-106752, 43, 145224, 0, 12)` | `datetime.timedelta(-106752, 43, 145224, 0, 12)` | + | minute | `-0Wu` | `datetime.timedelta(days=-1491309, seconds=78780)` | `datetime.timedelta(days=-1491309, seconds=78780)` | + | second | `-0Wv` | `datetime.timedelta(-24856, 53, 0, 0, 45, 20)` | `datetime.timedelta(-24856, 53, 0, 0, 45, 20)` | + | time | `-0Wt` | `datetime.timedelta(-25, 36, 353000, 0, 28, 3)` | `datetime.timedelta(-25, 36, 353000, 0, 28, 3)` | + +=== ".np()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|--------|--------------------------------------------------|--------------------------------------------------| + | short | `-0Wh` | `np.int16(-32767)` | `np.int16(-32767)` | + | int | `-0Wi` | `np.int32(-2147483647)` | `np.int32(-2147483647)` | + | long | `-0Wj` | `np.int64(-9223372036854775807)` | `np.int64(-9223372036854775807)` | + | real | `-0We` | `np.float32('-inf')` | `np.float32('-inf')` | + | float | `-0w` | `np.float64('-inf')` | `np.float64('-inf')` | + | timestamp | `-0Wp` | `np.datetime64('1707-09-22T00:12:43.145224193')` | `np.datetime64('1707-09-22T00:12:43.145224193')` | + | month | `-0Wm` | `np.datetime64('-178954971-06')` | `np.datetime64('-178954971-06')` | + | date | `-0Wd` | `np.datetime64('-5877611-06-23')` | `np.datetime64('-5877611-06-23')` | + | timespan | `-0Wn` | `np.timedelta64(-9223372036854775807, 'ns')` | `np.timedelta64(-9223372036854775807, 'ns')` | + | minute | `-0Wu` | `np.timedelta64(-2147483647, 'm')` | `np.timedelta64(-2147483647, 'm')` | + | second | `-0Wv` | `np.timedelta64(-2147483647, 's')` | `np.timedelta64(-2147483647, 's')` | + | time | `-0Wt` | `np.timedelta64(-2147483647, 'ms')` | `np.timedelta64(-2147483647, 'ms')` | + +=== ".pd()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|--------|-------------------------------------------------|-------------------------------------------------| + | short | `-0Wh` | `np.int16(-32767)` | `np.int16(-32767)` | + | int | `-0Wi` | `np.int32(-2147483647)` | `np.int32(-2147483647)` | + | long | `-0Wj` | `np.int64(-9223372036854775807)` | `np.int64(-9223372036854775807)` | + | real | `-0We` | `np.float32('-inf')` | `np.float32('-inf')` | + | float | `-0w` | `np.float64('-inf')` | `np.float64('-inf')` | + | timestamp | `-0Wp` | `pd.Timestamp('1707-09-22 00:12:43.145224193')` | `pd.Timestamp('1707-09-22 00:12:43.145224193')` | + | month | `-0Wm` | `Timestamp('-178954971-06-01 00:00:00')` **[1]** | `Timestamp('-178954971-06-01 00:00:00')` **[1]** | + | date | `-0Wd` | `Timestamp('-5877611-06-23 00:00:00')` **[1]** | `Timestamp('-5877611-06-23 00:00:00')` **[1]** | + | timespan | `-0Wn` | `pd.Timedelta(-9223372036854775807, 'ns')` | `pd.Timedelta(-9223372036854775807, 'ns')` | + | minute | `-0Wu` | `Timedelta('-1491309 days +21:53:00')` **[1]** | `Timedelta('-1491309 days +21:53:00')` **[1]** | + | second | `-0Wv` | `pd.Timedelta(-2147483647, 's')` | `pd.Timedelta(-2147483647, 's')` | + | time | `-0Wt` | `pd.Timedelta(-2147483647, 'ms')` | `pd.Timedelta(-2147483647, 'ms')` | + + !!! warning "Pandas version requirement" + + - **[1]** These conversions require Pandas 2 or later. Pandas 1 raises `Values out of range` because its constructors cannot create these values. + +=== ".pa()" + + | **Data type** | **q value** | **Atom conversion** | **Vector conversion** | + |-----------|--------|-------------------------------------------------|----------------------------------------------------------------------------------------------| + | short | `-0Wh` | `np.int16(-32767)` | `` | + | int | `-0Wi` | `np.int32(-2147483647)` | `` | + | long | `-0Wj` | `np.int64(-9223372036854775807)` | `` | + | real | `-0We` | `np.float32('-inf')` | `` | + | float | `-0w` | `np.float64('-inf')` | `` | + | timestamp | `-0Wp` | `pd.Timestamp('1707-09-22 00:12:43.145224193')` | `` | + | month | `-0Wm` | `Timestamp('-178954971-06-01 00:00:00')` **[1]** | **Error [2]** | + | date | `-0Wd` | `Timestamp('-5877611-06-23 00:00:00')` **[1]** | **Error [3]** | + | timespan | `-0Wn` | `pd.Timedelta(-9223372036854775807, 'ns')` | `` | + | minute | `-0Wu` | `Timedelta('-1491309 days +21:53:00')` **[1]** | **Error [4]** | + | second | `-0Wv` | `pd.Timedelta(-2147483647, 's')` | `` | + | time | `-0Wt` | `pd.Timedelta(-2147483647, 'ms')` | `` | + + !!! warning "Conversion limitations" + + - **[1]** These conversions require Pandas 2 or later. Pandas 1 raises `Values out of range` because its constructors cannot create these values. + - **[2]** PyArrow raises `pyarrow.lib.ArrowNotImplementedError: Unsupported datetime64 time unit`. + - **[3]** The conversion raises `OverflowError: days=-2147472690; must have magnitude <= 999999999`. + - **[4]** PyArrow raises `pyarrow.lib.ArrowNotImplementedError: Unsupported timedelta64 time unit`. #### Infinite weirdness diff --git a/docs/user-guide/fundamentals/query/index.md b/docs/user-guide/fundamentals/query/index.md index 88501c6..b5047f5 100644 --- a/docs/user-guide/fundamentals/query/index.md +++ b/docs/user-guide/fundamentals/query/index.md @@ -20,4 +20,4 @@ _This page provides an introduction to querying your data using KDB-X Python._ ## Next Steps - If you don't have a historical database available see [here](../../advanced/database/index.md). -- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.ipynb). +- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.md#25-table). diff --git a/docs/user-guide/fundamentals/query/perf.md b/docs/user-guide/fundamentals/query/perf.md index 041e466..07a919e 100644 --- a/docs/user-guide/fundamentals/query/perf.md +++ b/docs/user-guide/fundamentals/query/perf.md @@ -87,4 +87,4 @@ To optimize frequently called SQL queries you can make use of the [prepare](../. - Learn how to query your data using the KDB-X Python Query API [here](pyquery.md). - If you don't have a historical database available see [here](../../advanced/database/index.md). -- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.ipynb). +- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.md#25-table). diff --git a/docs/user-guide/fundamentals/query/pyquery.md b/docs/user-guide/fundamentals/query/pyquery.md index b7c0231..119f4e6 100644 --- a/docs/user-guide/fundamentals/query/pyquery.md +++ b/docs/user-guide/fundamentals/query/pyquery.md @@ -152,13 +152,13 @@ The `columns` keyword provides the ability to access columnar data by name or ap ```python >>> trades.delete(columns=kx.Column('date')) pykx.Table(pykx.q(' - sym price - ------------- - AAPL 145.6259 - MSFT 533.9187 - MSFT 17.17696 - GOOG 916.1286 - AAPL 140.0383 + sym price size + ------------------ + AAPL 145.6259 19 + MSFT 533.9187 92 + MSFT 17.17696 7 + GOOG 916.1286 60 + AAPL 140.0383 54 .. ')) ``` @@ -261,7 +261,6 @@ The `columns` keyword provides the ability to access columnar data by name or ap 140.0383 54 .. ')) - ')) ``` === "exec" @@ -269,7 +268,7 @@ The `columns` keyword provides the ability to access columnar data by name or ap ```python >>> trades.exec(columns=kx.Column('date') & kx.Column('price')) pykx.Dictionary(pykx.q(' - date | 2022.01.01 2022.01.02 2020.0.. + date | 2022.01.01 2022.01.02 2022.0.. price| 145.6259 533.9187 17.176.. ')) ``` @@ -302,12 +301,12 @@ The `columns` keyword provides the ability to access columnar data by name or ap multiPrice| 291.2518 1067.837 34.35.. symName | AAPL MSFT MSFT .. ')) - >>> trades.exec(columns=(2 * kx.Column('price', name='multiPrice') & - ... kx.Column('sym', name='symName')) - pykx.Dictionary(pykx.q(' - multiPrice| 291.2518 1067.837 34.35.. - symName | AAPL MSFT MSFT .. - ')) + >>> trades.exec(columns=(2 * kx.Column('price', name='multiPrice')) & + ... kx.Column('sym', name='symName')) + pykx.Dictionary(pykx.q(' + multiPrice| 291.2518 1067.837 34.35.. + symName | AAPL MSFT MSFT .. + ')) ``` === "update" @@ -346,7 +345,7 @@ The `columns` keyword provides the ability to access columnar data by name or ap pykx.Table(pykx.q(' maxPrice -------- - 993.6284 + 989.3873 ')) ``` @@ -460,20 +459,21 @@ By default this parameter has a value `None` which is equivalent to not filterin === "select" ```python + >>> import datetime >>> trades.select(where=(kx.Column('sym') == 'GOOG') & (kx.Column('date') == datetime.date(2022, 1, 1))) pykx.Table(pykx.q(' - sym date price - ------------------------ - GOOG 2022.01.01 480.9078 - GOOG 2022.01.01 454.5668 - GOOG 2022.01.01 790.2208 - GOOG 2022.01.01 296.6022 - GOOG 2022.01.01 727.6113 + sym date price size + ----------------------------- + GOOG 2022.01.01 872.5719 78 + GOOG 2022.01.01 465.9199 59 + GOOG 2022.01.01 477.2416 16 + GOOG 2022.01.01 745.6742 7 + GOOG 2022.01.01 472.7866 7 .. ')) >>> trades.select(where=[ ... kx.Column('sym') == 'GOOG', - ... kx.Column('date') == datetime.date(2022, 1, 1) + ... kx.Column('date') == datetime.date(2022, 1, 1) ... ]) >>> from datetime import date >>> trades.select(columns=kx.Column('price').wavg(kx.Column('size')), @@ -547,15 +547,15 @@ When both a `columns` and `by` clause are passed to a select query without use o pykx.KeyedTable(pykx.q(' date sym | price .. ---------------| ------------------------------------------------------------.. -2022.01.01 AAPL| 131.6095 236.3145 140.4332 839.3869 843.3531 641.2171 104.81.. -2022.01.01 GOOG| 480.9078 454.5668 790.2208 296.6022 727.6113 341.9665 609.77.. -2022.01.01 MSFT| 556.9152 755.6175 865.9657 714.9804 179.5444 149.734 67.0821.. -2022.01.02 AAPL| 441.8975 379.1373 659.8286 531.1731 975.3188 613.6512 603.99.. -2022.01.02 GOOG| 446.898 664.8273 648.3929 240.1062 119.6 774.3718 449.4149 8.. -2022.01.02 MSFT| 699.0336 387.7172 588.2985 725.8795 842.5805 646.37 593.7708.. -2022.01.03 AAPL| 793.2503 621.7243 570.4403 626.2866 263.992 153.475 123.7397.. -2022.01.03 GOOG| 586.263 777.3633 834.1404 906.9809 617.6205 179.6328 100.041.. -2022.01.03 MSFT| 633.3324 39.47309 682.9453 867.1843 483.0873 851.2139 318.93.. +2022.01.01 AAPL| 145.6259 636.4009 834.6557 190.0607 941.4561 728.0614 783.22.. +2022.01.01 GOOG| 872.5719 465.9199 477.2416 745.6742 472.7866 340.4314 461.68.. +2022.01.01 MSFT| 829.0066 242.9977 382.8027 873.4147 989.3873 113.8024 .. +2022.01.02 AAPL| 140.0383 876.0921 418.4969 964.1979 235.2337 794.7449 273.06.. +2022.01.02 GOOG| 276.1598 466.191 16.11913 184.1156 380.0184 947.9831 427.323.. +2022.01.02 MSFT| 533.9187 17.17696 603.3717 392.5541 399.0626 160.1576 116.92.. +2022.01.03 AAPL| 952.2597 932.1299 970.522 131.8826 599.2828 28.98133 229.389.. +2022.01.03 GOOG| 916.1286 230.7602 251.2994 354.463 157.0345 578.8499 704.611.. +2022.01.03 MSFT| 282.4291 46.11964 532.7252 241.2059 79.21147 429.1606 399.60.. ')) ``` @@ -566,15 +566,15 @@ Adding an aggregation function allows this aggregation to be run on a column wit pykx.KeyedTable(pykx.q(' date sym | price ---------------| -------- -2022.01.01 AAPL| 843.3531 -2022.01.01 GOOG| 790.2208 -2022.01.01 MSFT| 865.9657 -2022.01.02 AAPL| 975.3188 -2022.01.02 GOOG| 886.0093 -2022.01.02 MSFT| 993.6284 -2022.01.03 AAPL| 843.9354 -2022.01.03 GOOG| 914.6929 -2022.01.03 MSFT| 867.1843 +2022.01.01 AAPL| 941.4561 +2022.01.01 GOOG| 975.5566 +2022.01.01 MSFT| 989.3873 +2022.01.02 AAPL| 982.6867 +2022.01.02 GOOG| 954.7454 +2022.01.02 MSFT| 979.3561 +2022.01.03 AAPL| 970.522 +2022.01.03 GOOG| 916.1286 +2022.01.03 MSFT| 532.7252 ')) ``` @@ -603,11 +603,11 @@ AAPL 2022.01.02 140.0383 54 56.09317 ```python >>> trades.select(by=kx.Column('sym')) pykx.KeyedTable(pykx.q(' - sym | date price - ----| ------------------- - AAPL| 2022.01.02 955.4843 - GOOG| 2022.01.02 886.0093 - MSFT| 2022.01.01 719.9879 + sym | date price size + ----| ------------------------ + AAPL| 2022.01.02 726.5864 55 + GOOG| 2022.01.01 975.5566 55 + MSFT| 2022.01.03 194.547 42 ')) ``` @@ -654,9 +654,9 @@ qSQL equivalent query for comparison: ```python >>> kx.q('select from trades where price=max price') pykx.Table(pykx.q(' -sym date price ------------------------- -AAPL 2022.01.01 983.0794 +sym date price size +----------------------------- +MSFT 2022.01.01 989.3873 42 ')) ``` @@ -665,9 +665,9 @@ Access query API off the table object: ```python >>> trades.select(where=kx.Column('price') == kx.Column('price').max()) pykx.Table(pykx.q(' -sym date price ------------------------- -AAPL 2022.01.01 983.0794 +sym date price size +----------------------------- +MSFT 2022.01.01 989.3873 42 ')) ``` @@ -676,9 +676,9 @@ Direct use of the `kx.q.qsql` query APIs taking the table as a parameter: ```python >>> kx.q.qsql.select(trades, where=kx.Column('price') == kx.Column('price').max()) pykx.Table(pykx.q(' -sym date price ------------------------- -AAPL 2022.01.01 983.0794 +sym date price size +----------------------------- +MSFT 2022.01.01 989.3873 42 ')) ``` @@ -687,9 +687,9 @@ Passing a string will query the table of that name in q memory: ```python >>> kx.q.qsql.select('trades', where=kx.Column('price') == kx.Column('price').max()) pykx.Table(pykx.q(' -sym date price ------------------------- -AAPL 2022.01.01 983.0794 +sym date price size +----------------------------- +MSFT 2022.01.01 989.3873 42 ')) ``` @@ -725,13 +725,13 @@ Using `&` on two `Column` objects will return a `QueryPhrase` which describes th [[pykx.Operator(pykx.q('=')), 'sym', [pykx.SymbolAtom(pykx.q('`GOOG'))]], [pykx.Operator(pykx.q('>')), 'price', pykx.LongAtom(pykx.q('500'))]] >>> trades.select(where=qp) pykx.Table(pykx.q(' -sym date price ------------------------- -GOOG 2022.01.03 976.1246 -GOOG 2022.01.02 716.2858 -GOOG 2022.01.03 872.5027 -GOOG 2022.01.02 962.5156 -GOOG 2022.01.01 589.7202 +sym date price size +----------------------------- +GOOG 2022.01.03 916.1286 60 +GOOG 2022.01.03 578.8499 46 +GOOG 2022.01.03 704.6117 70 +GOOG 2022.01.01 872.5719 78 +GOOG 2022.01.02 947.9831 41 .. ')) ``` @@ -750,17 +750,17 @@ Using `|` on two `Column` objects will return a `Column` object. >>> c =(kx.Column('price') < 100) | (kx.Column('price') > 500) >>> type(c) ->>> c._value +>>> c._data [pykx.Operator(pykx.q('|')), [pykx.Operator(pykx.q('<')), 'price', pykx.LongAtom(pykx.q('100'))], [pykx.Operator(pykx.q('>')), 'price', pykx.LongAtom(pykx.q('500'))]] >>> trades.select(where=c) pykx.Table(pykx.q(' -sym date price ------------------------- -AAPL 2022.01.01 542.6371 -AAPL 2022.01.01 77.57332 -MSFT 2022.01.01 637.4637 -GOOG 2022.01.03 976.1246 -MSFT 2022.01.03 539.6816 +sym date price size +----------------------------- +MSFT 2022.01.02 533.9187 92 +MSFT 2022.01.02 17.17696 7 +GOOG 2022.01.03 916.1286 60 +MSFT 2022.01.03 46.11964 93 +MSFT 2022.01.02 603.3717 6 .. ')) ``` @@ -842,13 +842,13 @@ The following are a few examples of this various operations in use ```python >>> trades.select(where=kx.Column('price') >= kx.Column('price').avg() / 2) pykx.Table(pykx.q(' - sym date price - ------------------------ - AAPL 2022.01.01 542.6371 - MSFT 2022.01.01 637.4637 - GOOG 2022.01.03 976.1246 - MSFT 2022.01.03 539.6816 - GOOG 2022.01.02 716.2858 + sym date price size + ----------------------------- + MSFT 2022.01.02 533.9187 92 + GOOG 2022.01.03 916.1286 60 + MSFT 2022.01.03 282.4291 98 + MSFT 2022.01.02 603.3717 6 + MSFT 2022.01.02 392.5541 34 .. ')) ``` @@ -931,13 +931,13 @@ The following example shows this in action >>> kx.q['filter']='GOOG' >>> trades.select(where=kx.Column('sym') == kx.Variable('filter')) pykx.Table(pykx.q(' -sym date price ------------------------- -GOOG 2022.01.03 976.1246 -GOOG 2022.01.02 716.2858 -GOOG 2022.01.03 872.5027 -GOOG 2022.01.02 962.5156 -GOOG 2022.01.01 589.7202 +sym date price size +----------------------------- +GOOG 2022.01.03 916.1286 60 +GOOG 2022.01.03 230.7602 23 +GOOG 2022.01.02 276.1598 63 +GOOG 2022.01.03 251.2994 24 +GOOG 2022.01.03 354.463 87 .. ')) ``` @@ -1003,9 +1003,8 @@ Take for example the case where you want to find the stock information by symbol pykx.Table(pykx.q(' sym date price size ----------------------------- -MSFT 2022.01.03 977.1655 92 -AAPL 2022.01.02 996.8898 20 -GOOG 2022.01.03 971.9498 47 +MSFT 2022.01.01 989.3873 42 +GOOG 2022.01.01 975.5566 55 ')) ``` @@ -1036,6 +1035,7 @@ Calculate the maximum value of each row of a column `x` ```python >>> table = kx.Table(data={'x': [[10, 5, 4], [20, 30, 50], [1, 2, 3]]}) +>>> table pykx.Table(pykx.q(' x -------- @@ -1059,6 +1059,7 @@ Join the characters associated from two columns row wise using the `'` iterator ```python >>> table = kx.Table(data={'x': b'abc', 'y': b'def'}) +>>> table pykx.Table(pykx.q(' x y --- @@ -1082,6 +1083,7 @@ Join the characters `"_xy"` to all rows in a column `x` ```python >>> table = kx.Table(data={'x': b'abc', 'y': b'def'}) +>>> table pykx.Table(pykx.q(' x y --- @@ -1110,4 +1112,4 @@ Now that you have learnt how to query your data using the Pythonic API you may b For some further reading, here are some related topics: - If you don't have a historical database available see [here](../../advanced/database/index.md). -- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.ipynb). +- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.md#25-table). diff --git a/docs/user-guide/fundamentals/query/qquery.md b/docs/user-guide/fundamentals/query/qquery.md index 622cf85..6f8d21b 100644 --- a/docs/user-guide/fundamentals/query/qquery.md +++ b/docs/user-guide/fundamentals/query/qquery.md @@ -86,4 +86,4 @@ Now that you have learnt how to query your data using q you may be interested in For some further reading, here are some related topics: - If you don't have a historical database available see [here](../../advanced/database/index.md). -- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.ipynb). +- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.md#25-table). diff --git a/docs/user-guide/fundamentals/query/sql.md b/docs/user-guide/fundamentals/query/sql.md index 1b32124..181be4b 100644 --- a/docs/user-guide/fundamentals/query/sql.md +++ b/docs/user-guide/fundamentals/query/sql.md @@ -107,4 +107,4 @@ Now that you have learnt the fundamentals of how to query your data using the SQ For some further reading, here are some related topics: - If you don't have a historical database available see [here](../../advanced/database/index.md). -- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.ipynb). +- To learn about creating `pykx` Table objects see [here](../../../examples/interface-overview.md#25-table). diff --git a/docs/user-guide/fundamentals/temporal.md b/docs/user-guide/fundamentals/temporal.md index 0724363..839c5c8 100644 --- a/docs/user-guide/fundamentals/temporal.md +++ b/docs/user-guide/fundamentals/temporal.md @@ -138,3 +138,14 @@ pykx.SecondAtom(pykx.q('**:00:00')) >>> kx.q('99:59:59 +1').pd() Timedelta('4 days 04:00:00') ``` + +KDB-X Python raises an error when you convert a `#!python timedelta` value greater than `106751 days, 23:47:16.854775` to a `#!python kx.TimespanAtom`: + +```python +>>> import datetime +>>> kx.toq(datetime.timedelta(days=106751, seconds=85636, microseconds=854775)) +pykx.TimespanAtom(pykx.q('106751D23:47:16.854775000')) +>>> kx.toq(datetime.timedelta(days=106751, seconds=85636, microseconds=854776)) +... +OverflowError: Python int too large to convert to C long +``` diff --git a/mkdocs.yml b/mkdocs.yml index e70c393..14aec88 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ extra_css: # More accessible colors in the edited versions - stylesheets/2026-edited.css - stylesheets/prism-edited.css + - stylesheets/404.css extra_javascript: - https://code.kx.com/scripts/prism.js @@ -41,9 +42,14 @@ extra_javascript: - 'https://code.jquery.com/jquery-3.6.0.min.js' - 'https://use.fontawesome.com/3dbf29ed12.js' - scripts/strip-q-prompt-copy.js - # this relative path is needed to source from code.kx.com!!! - - "../../pykx/scripts/versions.js" - # Uncomment only for local test + - scripts/init_kapa_widget.js + - scripts/collapse-code.js + - scripts/search-highlight-clear.js + # absolute URL so the shared, symlinked versions.js loads on every page. + # The old "." relative hack is double-escaped to "&period;" by + # newer mkdocs-material, which 404s and breaks the version dropdown. + - https://code.kx.com/pykx/scripts/versions.js + # Uncomment only for local test # - 'scripts/versions.js' @@ -82,7 +88,8 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tilde - toc: - permalink: false + permalink: true + permalink_title: "" title: On this page plugins: @@ -127,11 +134,11 @@ plugins: - search - exclude-search: exclude: - - user-guide/advanced/Pandas_API.ipynb - - examples/interface-overview.ipynb - - examples/jupyter-integration.ipynb - - examples/db-management.ipynb - - examples/streaming/Evolving System.ipynb + - user-guide/advanced/Pandas_API* + - examples/jupyter-integration* + - examples/db-management* + - "examples/streaming/Evolving System*" + - examples/charting* - spellcheck: known_words: spelling.txt ignore_code: true # Ignore words in tags @@ -142,7 +149,6 @@ plugins: - comparisons.md # Skipped due to false positives - performance.md # Skipped due to false positives - user-guide/advanced/Pandas_API.ipynb - - examples/interface-overview.ipynb - examples/jupyter-integration.ipynb - examples/db-management.ipynb - examples/streaming/Evolving System.ipynb @@ -167,19 +173,20 @@ plugins: 'beta-features/streamlit.md': 'user-guide/advanced/streamlit.md' 'beta-features/threading.md': 'user-guide/advanced/threading.md' 'changelog.md': 'release-notes/changelog.md' - 'comparisons.md': 'getting-started/what_is_pykx.md' + 'comparisons.md': 'getting-started/what-is-kdb-x-python.md' 'evaluating.md': 'user-guide/fundamentals/evaluating.md' 'examples/threaded_execution/index.md': 'examples/threaded_execution/threading.md' 'examples/threaded_execution/threaded_execution.md': 'examples/threaded_execution/threading.md' - 'extras/comparisons.md': 'getting-started/what_is_pykx.md' + 'extras/comparisons.md': 'getting-started/what-is-kdb-x-python.md' 'extras/faq.md': 'help/faq.md' 'extras/known_issues.md': 'help/issues.md' 'faq.md': 'help/faq.md' - 'getting-started/interface_overview.md': 'getting-started/what_is_pykx.md' + 'getting-started/interface_overview.md': 'examples/interface-overview.md' + 'getting-started/what_is_pykx.md': 'getting-started/what-is-kdb-x-python.md' 'getting-started/q_magic_command.md': 'examples/jupyter-integration.ipynb' 'indexing.md': 'user-guide/fundamentals/indexing.md' 'installing.md': 'getting-started/installing.md' - 'interface_overview.md': 'getting-started/what_is_pykx.md' + 'interface_overview.md': 'examples/interface-overview.md' 'known_issues.md': 'help/issues.md' 'licenses.md': 'license.md' 'limitations.md': 'help/issues.md' @@ -215,6 +222,10 @@ theme: - content.code.annotate - content.action.edit - content.code.copy + - search.highlight + - navigation.tracking + - navigation.top + - toc.follow palette: - media: "(prefers-color-scheme: light)" scheme: kx-light @@ -231,13 +242,13 @@ theme: docs_dir: docs nav: - - Home: + - Home: - Overview: index.md - - Get Started: - - Install: getting-started/installing.md - - Quickstart guide: getting-started/quickstart.md + - About: getting-started/what-is-kdb-x-python.md + - Install: getting-started/installing.md + - Quickstart: getting-started/quickstart.md - Learn: - - What is KDB-X Python?: getting-started/what_is_pykx.md + - Fundamentals: examples/interface-overview.md - Objects and attributes: learn/objects.md - Databases: user-guide/advanced/database/index.md - Modes of operation: user-guide/advanced/modes.md @@ -313,7 +324,7 @@ nav: - Python to KDB-X: api/pykx-q-data/toq.md - Wrappers: api/pykx-q-data/wrappers.md - KDB-X to Python: api/pykx-q-data/type_conversions.md - - Pandas API: user-guide/advanced/Pandas_API.ipynb + - pandas API: user-guide/advanced/Pandas_API.ipynb - Exceptions: api/exceptions.md - Integrations: - NumPy: user-guide/advanced/numpy.md @@ -321,7 +332,6 @@ nav: - Python charting libraries: examples/charting.ipynb - Jupyter Integration: examples/jupyter-integration.ipynb - Examples: - - KDB-X Python Introduction Notebook: examples/interface-overview.ipynb - Subscriber: examples/subscriber/readme.md - Compress and encrypt: examples/compress_and_encrypt/readme.md - Database creation and management: examples/db-management.ipynb diff --git a/setup.py b/setup.py index 3f2cd03..a08c460 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,6 @@ 'Windows': 'w64', }[system] -py_minor_version = sys.version_info[1] debug = os.environ.get('PYKX_DEBUG', '').lower() in {'true', '1'} windows_vcpkg_content = script_dir.resolve()/'vcpkg'/'installed'/'x64-windows-static-md' @@ -50,7 +49,7 @@ windows_library_dirs = () if system != 'Windows' else (str(Path(sys.exec_prefix)/'libs'), str(Path(sys.base_exec_prefix)/'libs'), str(windows_vcpkg_content/'lib')) -windows_libraries = () if system != 'Windows' else ('psapi', 'q') +windows_libraries = () if system != 'Windows' else ('psapi', 'qdll') class CustomInstallCommand(install): @@ -111,7 +110,7 @@ def build_q_c_extension(self, compiler, lib, lib_ext, library=None): extra_preargs=[ *( ('-undefined dynamic_lookup',) if system == 'Darwin' else - ('/LD /Fepykxq.dll q.lib -I include',) if system == 'Windows' else () + ('/LD /Fepykxq.dll qdll.lib -I include',) if system == 'Windows' else () ), ], ), diff --git a/src/pykx/__init__.py b/src/pykx/__init__.py index 08fd45e..8898c01 100644 --- a/src/pykx/__init__.py +++ b/src/pykx/__init__.py @@ -7,12 +7,20 @@ import os import sys +if os.getenv('PYKX_LOADING_Q', '') == '': + os.environ['PYKX_LOADING_PYTHON'] = 'true' + +if os.getenv('PYKX_OLD_QHOME', '') != '' and os.getenv('PYKX_LOADING_PYTHON', '') != '': + os.environ['QHOME'] = os.getenv('PYKX_OLD_QHOME', "") +if os.getenv('PYKX_OLD_QPATH', '') != '' and os.getenv('PYKX_LOADING_PYTHON', '') != '': + os.environ['QPATH'] = os.getenv('PYKX_OLD_QPATH', "") + if os.getenv('PYKX_LOADED_UNDER_Q') == 'True': sys.exit(0) # Attempt to import PyArrow early to get ahead of others (e.g. Pandas) who would try to import it # without guarding against segfaults. Skip this if we're just doing a qinit check. -if os.environ.get('PYKX_QINIT_CHECK') is None: +if os.getenv('PYKX_QINIT_CHECK', '') == '': try: from ._pyarrow import pyarrow except ImportError: # nocov @@ -561,3 +569,20 @@ def deactivate_numpy_allocator(): def __dir__(): return __all__ + + +envlist = ( + 'PYKX_UNDER_Q', + 'PYKX_UNDER_PYTHON', + 'PYKX_Q_LOADED_MARKER', + 'PYKX_LOADED_UNDER_Q', + 'PYKX_LOADING_PYTHON', + 'PYKX_LOADING_Q', + 'PYKX_QINIT_CHECK', +) +if os.getenv('PYKX_LOADING_PYTHON', '') != '': + for x in envlist: + os.environ[x] = '' + del os.environ[x] + if licensed: + q(f'setenv[`{x}; ""]') diff --git a/src/pykx/_tcore.c b/src/pykx/_tcore.c index cbced2a..3105955 100644 --- a/src/pykx/_tcore.c +++ b/src/pykx/_tcore.c @@ -487,6 +487,7 @@ struct QInit { void* q_thread_init(void* _qini) { struct QInit* qini = (struct QInit*)_qini; qinit_rc = _qinit(qini->argc, qini->argv, qini->qhome, qini->qlic, qini->qqq); + // `qini` is owned and freed by q_init once it observes qinit_rc; do not free here. pthread_mutex_lock(&init_mutex); pthread_cond_signal(&init); pthread_mutex_unlock(&init_mutex); @@ -546,12 +547,19 @@ int q_init(int argc, char** argv, char* qhome, char* qlic, char* qqq) { qini->qqq = qqq; // TODO: ADD COMMENT qinit_rc = -256; int rc = pthread_create(&q_thread, NULL, q_thread_init, (void*)qini); + if (rc != 0) { + // q_thread_init never runs, so free here and avoid waiting on `init` forever. + free(qini); + return rc; + } pthread_mutex_lock(&init_mutex); while (qinit_rc == -256) { pthread_cond_wait(&init, &init_mutex); } pthread_mutex_unlock(&init_mutex); + // Safe to free now: q_thread_init finished reading `qini` before signalling `init`. + free(qini); return qinit_rc; } diff --git a/src/pykx/config.py b/src/pykx/config.py index ea5009c..c4a9b9e 100644 --- a/src/pykx/config.py +++ b/src/pykx/config.py @@ -34,6 +34,7 @@ # If PYKX_CONFIGURATION_LOCATION is not set it will search '.' pykx_config_location = os.path.expanduser(os.getenv('PYKX_CONFIGURATION_LOCATION', '')) pykx_config_profile = os.getenv('PYKX_PROFILE', 'default') +unlicensed_reason = None def _get_config_value(param, default): @@ -55,10 +56,6 @@ def _is_enabled(param, cmdflag=False): return env_config or (cmdflag and cmdflag in qargs) -def _is_set(envvar): - return os.getenv(envvar, None) - - pykx_config_locs = [Path.home()/'.kx/config-pykx'] if pykx_config_location != '': pykx_config_locs = [Path(pykx_config_location)] + pykx_config_locs @@ -157,7 +154,7 @@ def _get_qcfg_values(): def _get_qhome(): qhome_value = _get_config_value('QHOME', None) - if qhome_value is not None: + if qhome_value: try: qhome = Path(qhome_value).resolve(strict=True) return qhome @@ -174,7 +171,15 @@ def _get_qhome(): def _get_qpath(qhome): qpath = _get_config_value('QPATH', None) - os.environ['QPATH'] = str(qhome/'mod' if qpath is None else qpath+':'+str(qhome/'mod')) + mod = str(qhome / 'mod') + if qpath is None or qpath == '': + result = mod + elif mod in qpath.split(os.pathsep): + result = qpath + else: + result = qpath + os.pathsep + mod + os.environ['QPATH'] = result + return result if 'QHOME' in _qcfg_values: @@ -187,6 +192,8 @@ def _get_qpath(qhome): qhome = _get_qhome() qpath = _get_qpath(qhome) +os.environ['PYKX_OLD_QHOME'] = str(qhome) if qhome is not None else "" +os.environ['PYKX_OLD_QPATH'] = str(qpath) if qpath is not None else "" # License search if 'QLIC' in _qcfg_values: @@ -285,6 +292,7 @@ def _license_install_B64(license, license_type): # pragma: no cover def _license_check(lic_type, lic_encoding, lic_variable): # pragma: no cover + global unlicensed_reason license_content = None lic_name = lic_type + '.lic' lic_file = qlic / lic_name @@ -299,6 +307,7 @@ def _license_check(lic_type, lic_encoding, lic_variable): # pragma: no cover f' License location: {qlic}/{lic_type}.lic\n'\ 'Reason: License content matches supplied Environment variable' print(conflict_message) + unlicensed_reason = f"Updating license failed. {lic_variable} contents matched contents of {qlic}/{lic_type}.lic." # noqa: E501 return False else: return _license_install_B64(lic_encoding, lic_name) @@ -333,7 +342,7 @@ def _unlicensed_config(unlicensed_message): def _license_install(intro=None, return_value=False, license_check=False, license_error=None): # noqa: - + global unlicensed_reason if not hasattr(sys, 'ps1'): # Exit if running in a non-interactive session return False @@ -371,6 +380,7 @@ def _license_install(intro=None, return_value=False, license_check=False, licens f' QHOME {qhome if qhome else "Not Set"}\n\n'\ 'Running KDB-X Python in unlicensed mode has reduced functionality.\n'\ 'Would you like to install a license? [Y/n]: ' + unlicensed_reason = "No license was found." root = 'C:\\path\\to\\' if platform.system() == 'Windows' else '~/path/to/' continue_license = input(first_user if intro is None else intro) if continue_license in ('n', 'N'): @@ -458,6 +468,22 @@ def _license_install(intro=None, return_value=False, license_check=False, licens licensed = False _pykx_force_unlicensed = ('--unlicensed' in qargs or _is_enabled('PYKX_UNLICENSED', '--unlicensed')) or light_load # noqa: E501 + +try: + config_unlicensed_flag = pykx_profile_content['PYKX_UNLICENSED'] +except KeyError: + config_unlicensed_flag = False + +if _pykx_force_unlicensed: + if '--unlicensed' in qargs: + unlicensed_reason = ("--unlicensed passed to args.") + elif light_load: + unlicensed_reason = ("light_load enabled.") + elif os.environ.get('PYKX_UNLICENSED') and unlicensed_reason is None: + unlicensed_reason = ("The environment variable PYKX_UNLICENSED is set to True.") + elif config_unlicensed_flag: + unlicensed_reason = (f"PYKX_UNLICENSED is set to True in the config file {pykx_config_location}.") # noqa: E501 + _pykx_force_licensed = ('--licensed' in qargs or _is_enabled('PYKX_LICENSED', '--licensed')) and not light_load # noqa: E501 qlib_location = Path(_get_config_value('PYKX_Q_LIB_LOCATION', pykx_libs_dir)) @@ -510,6 +536,24 @@ def _set_licensed(licensed_): licensed = licensed_ +def _set_unlicensed_reason(unlicensed_reason_): + global unlicensed_reason + unlicensed_reason = unlicensed_reason_ + + +def _get_unlicensed_reason(): + return unlicensed_reason + + +def _set_qdebug(qdebug_): + global pykx_qdebug + pykx_qdebug = qdebug_ + + +def _get_qdebug(): + return pykx_qdebug + + def _set_keep_local_times(keep_local_times_): global keep_local_times keep_local_times = keep_local_times_ diff --git a/src/pykx/core.pxd b/src/pykx/core.pxd index 3af8dee..845dfb2 100644 --- a/src/pykx/core.pxd +++ b/src/pykx/core.pxd @@ -94,7 +94,7 @@ cdef K (*ktj)(short _type, long long x) cdef K (*ktn)(int _type, long long length) cdef K (*ku)(U x) cdef K (*kz)(double x) -cdef void (*m9)() +cdef void (*_m9)() cdef int (*okx)(K x) cdef K (*orr)(const char*) cdef void (*r0)(K k) diff --git a/src/pykx/core.pyx b/src/pykx/core.pyx index a052f77..a116fc0 100644 --- a/src/pykx/core.pyx +++ b/src/pykx/core.pyx @@ -74,8 +74,8 @@ cdef void* _q_handle # or fallback to unlicensed mode. # Data is passed in via the `PYKX_QINIT_CHECK` env var to reduce `pykx` import time - why do the # work to gather that data when the parent process has already done it? -qinit_check_data = os.environ.get('PYKX_QINIT_CHECK') -if qinit_check_data is not None: # nocov +qinit_check_data = os.getenv('PYKX_QINIT_CHECK', '') +if qinit_check_data != '': # nocov _core_q_lib_path, _qhome_str, _qlic_str, _qargs = qinit_check_data.split(';') # nocov import shlex # nocov _qargs = list(shlex.split(_qargs)) # nocov @@ -95,7 +95,7 @@ import subprocess import sys from .config import find_core_lib, qargs, qhome, qlic, pykx_lib_dir, \ - release_gil, _set_licensed, under_q, use_q_lock, _qlic + release_gil, _set_licensed, _set_unlicensed_reason, under_q, use_q_lock, _qlic from .exceptions import PyKXException, PyKXWarning final_qhome = str(qhome if ignore_qhome else pykx_lib_dir) @@ -301,7 +301,7 @@ if not pykx_threading: _capout_msg = f'Captured output from initialization attempt:\n{_qinit_output}' _paths_checked = f' QLIC {_qlic if _qlic else "Not Set"}\n'\ f' QHOME {qhome if qhome else "Not Set"}' - _lic_location = f'License used:\n {lic_path}' + _lic_location = f'\n {lic_path}' else: _capout_msg = '' # nocov - this can only occur under extremely weird circumstances. _lic_location = '' # nocov - this additional line is to ensure this code path is covered. @@ -313,6 +313,7 @@ if not pykx_threading: f'{_lic_location}\n\n'\ 'Would you like to renew your license? (Selecting no will proceed with unlicensed mode) [Y/n]: ' _license_message = _license_install(_exp_license, True, True, 'exp') + _set_unlicensed_reason(f"Your license is expired: {_lic_location}") elif re.compile('licen[cs]e error: embedq').search(_capout_msg): _ce_license = 'You appear to be using a non KDB-X Python enabled license.\n\n'\ f'{_capout_msg}\n\n'\ @@ -321,6 +322,7 @@ if not pykx_threading: 'has reduced functionality.\nWould you like to install '\ 'a license? [Y/n]: ' _license_message = _license_install(_ce_license, True) + elif re.compile('licen[cs]e error: upd').search(_capout_msg): _upd_license = 'Your installed license is out of date for this version'\ ' of KDB-X Python and must be updated.\n\n'\ @@ -328,12 +330,14 @@ if not pykx_threading: f'{_lic_location}\n\n'\ 'Would you like to install an updated license? [Y/n]: ' _license_message = _license_install(_upd_license, True) + _set_unlicensed_reason(f"Your license requires an update: {_lic_location}") elif re.compile('licen[cs]e error: k[xc4].lic').search(_capout_msg) or re.compile('licen[cs]e error: badmsg').search(_capout_msg): _k_license = '\nThe KDB-X Python license found is corrupt or incompatible.\n\n'\ f'{_capout_msg}\n\n'\ f'{_lic_location}\n\n'\ 'Would you like to install a new license? [Y/n]: ' _license_message = _license_install(_k_license, True) + _set_unlicensed_reason(f"Your license is corrupt or incompatible: {_lic_location}") else: warning = f'Failed to initialize KDB-X Python successfully with the following error: {_capout_msg}\n\n' if _lic_location == '': @@ -344,6 +348,7 @@ if not pykx_threading: _missing_license = 'Running KDB-X Python in unlicensed mode has reduced functionality.\n\n'\ 'Would you like to install a license? (Selecting no will proceed with unlicensed mode) [Y/n]: ' _license_message = _license_install(_missing_license, True) + _set_unlicensed_reason(f"Your license could not be found.") if (not _license_message) and _qinit_check_proc.returncode: if _pykx_force_licensed: raise PyKXException(f'Failed to initialize embedded q.{_capout_msg}') @@ -360,7 +365,7 @@ if not pykx_threading: _qinit_args = {'qhome': final_qhome, 'qlic': qlic, 'ignore_qhome': ignore_qhome, 'qargs': list(qargs)} raise PyKXException(f'Non-zero qinit following license install with configuration: {_qinit_args}\n' f'failed with output: {_qinit_output}') - if 'QHOME' in os.environ and not ignore_qhome: + if 'QHOME' in os.environ and not ignore_qhome and os.environ['QHOME'] != '': # Only link the user's QHOME to KDB-X Python's QHOME if the user actually set $QHOME. # Note that `pykx.qhome` has a default value of `./q`, as that is the behavior # employed by q. @@ -432,6 +437,8 @@ cpdef shutdown_thread(): if pykx_threading: _shutdown_thread() +cpdef m9(): + _m9() b9 = dlsym(_q_handle, sym_name('b9')) d9 = dlsym(_q_handle, sym_name('d9')) @@ -471,7 +478,7 @@ ktj = dlsym(_q_handle, sym_name('ktj')) ktn = dlsym(_q_handle, sym_name('ktn')) ku = dlsym(_q_handle, sym_name('ku')) kz = dlsym(_q_handle, sym_name('kz')) -m9 = dlsym(_q_handle, sym_name('m9')) +_m9 = dlsym(_q_handle, sym_name('m9')) okx = dlsym(_q_handle, sym_name('okx')) orr = dlsym(_q_handle, sym_name('orr')) r0 = dlsym(_q_handle, sym_name('r0')) diff --git a/src/pykx/db.py b/src/pykx/db.py index d49df09..bdebc2d 100644 --- a/src/pykx/db.py +++ b/src/pykx/db.py @@ -681,7 +681,7 @@ def add_column(self, ['testTable'] >>> db.list_columns('testTable') ['month', 'sym', 'time', 'price', 'size'] - >>> db.add_column('testTable', 'test', kx.IntAtom.null) + >>> db.add_column('testTable', 'test', kx.IntAtom.null) >>> db.list_columns('testTable') ['month', 'sym', 'time', 'price', 'size'] ``` @@ -1171,7 +1171,7 @@ def partition_count(self, *, subview: Optional[list] = None) -> k.Dictionary: ```python >>> import pykx as kx >>> db = kx.DB(path = 'newDB') - >>> db.partition_count(sub_view = kx.q('2020.01 2020.02m')) + >>> db.partition_count(subview = kx.q('2020.01 2020.02m')) pykx.Dictionary(pykx.q(' | trades quotes -------| ------------- @@ -1218,7 +1218,7 @@ def subview(self, view: list = None) -> None: ```python >>> import pykx as kx >>> db = kx.DB(path = 'newDB') - >>> db.subview(kx.q('2020.02 2020.03m') + >>> db.subview(kx.q('2020.02 2020.03m')) >>> kx.q.qsql.select(db.trades, 'month') pykx.Table(pykx.q(' month @@ -1278,7 +1278,7 @@ def enumerate(self, table: str, *, sym_file: Optional[str] = None) -> k.Table: ... 'x': kx.random.random(N, ['a', 'b', 'c']), ... 'x1': kx.random.random(N, 1.0), ... 'x2': kx.random.random(N, 10) - ... } + ... }) >>> tab = db.enumerate(tab) >>> tab['x'] pykx.EnumVector(pykx.q('`sym$`a`b`a`c`b..')) @@ -1294,7 +1294,7 @@ def enumerate(self, table: str, *, sym_file: Optional[str] = None) -> k.Table: ... 'x': kx.random.random(N, ['a', 'b', 'c']), ... 'x1': kx.random.random(N, 1.0), ... 'x2': kx.random.random(N, 10) - ... } + ... }) >>> tab = db.enumerate(tab, sym_file = 'mysym') >>> tab['x'] pykx.EnumVector(pykx.q('`mysym$`a`b`a`c`b..')) diff --git a/src/pykx/embedded_q.py b/src/pykx/embedded_q.py index 7838858..f4bdfad 100644 --- a/src/pykx/embedded_q.py +++ b/src/pykx/embedded_q.py @@ -11,7 +11,7 @@ from . import toq from . import wrappers from . import schema -from .config import find_core_lib, licensed, pykx_dir, pykx_libs_dir, pykx_qdebug, pykx_threading, qargs, qce, skip_under_q, suppress_warnings, pykx_debug_insights # noqa +from .config import find_core_lib, _get_qdebug, licensed, pykx_dir, pykx_libs_dir, pykx_threading, qargs, qce, skip_under_q, suppress_warnings, pykx_debug_insights # noqa from .core import keval as _keval from .exceptions import FutureCancelled, LicenseException, NoResults, PyKXException, PyKXWarning, QError # noqa from ._wrappers import _factory as factory @@ -144,14 +144,14 @@ def __init__(self): # noqa .pykx.util.loadfile["{kxic_path}";"{kxic_file}"] ]; ''' - if os.getenv('PYKX_UNDER_Q') is None: + if os.getenv('PYKX_UNDER_Q', '') == '': os.environ['PYKX_UNDER_PYTHON'] = 'true' code += 'setenv[`PYKX_UNDER_PYTHON;"true"];' code += f'2:[`$"{pykx_qlib_path}";(`k_pykx_init; 2)][`$"{find_core_lib("q").as_posix()}";{"1b" if pykx_threading else "0b"}];' # noqa: E501 code += f'`.pykx.modpow set {{((`$"{pykx_qlib_path}") 2: (`k_modpow; 3))["j"$x;"j"$y;$[z~(::);(::);"j"$z]]}};' # noqa: E501 else: suffix = "" - if os.getenv("PYKXQ_THREADING") is not None: + if os.getenv("PYKXQ_THREADING", '') != '': suffix = "_m" code += f'2:[`$"{pykx_qlib_path}q{suffix}";(`k_pykx_init; 2)][`$"{find_core_lib("q").as_posix()}";{"1b" if pykx_threading else "0b"}];' # noqa: E501 code += f'`.pykx.modpow set {{((`$"{pykx_qlib_path}q{suffix}") 2: (`k_modpow; 3))["j"$x;"j"$y;$[z~(::);(::);"j"$z]]}};' # noqa: E501 @@ -167,8 +167,8 @@ def __init__(self): # noqa PyKXWarning) else: eprint(f'WARN: Failed to load KX Insights Core library {lib!r}.') - if (not skip_under_q and os.getenv('PYKX_Q_LOADED_MARKER') != 'loaded' - and os.getenv('PYKX_UNDER_Q') is None + if (not skip_under_q and os.getenv('PYKX_Q_LOADED_MARKER', '') != 'loaded' + and os.getenv('PYKX_UNDER_Q', '') == '' ): os.environ['PYKX_Q_LOADED_MARKER'] = 'loaded' self._call('setenv[`PYKX_Q_LOADED_MARKER; "loaded"]', skip_debug=True) @@ -191,7 +191,7 @@ def __init__(self): # noqa ) self._call('.pykx.setdefault[enlist"k"]', skip_debug=True) if (self._call('@[{x in " " vs .z.l 7};"COMMUNITY";{0b}]', skip_debug=True).py() - and os.getenv('PYKX_UNDER_Q') is None): + and os.getenv('PYKX_UNDER_Q', '') == ''): print(("\nWelcome to KDB-X Community Edition!\n" "For Community support, please visit https://kx.com/slack\n" "Tutorials can be found at https://github.com/KxSystems/tutorials\n" @@ -235,7 +235,7 @@ def __call__(self, if len(args) > 8: raise TypeError('Too many arguments - q queries cannot have more than 8 parameters') query = wrappers.CharVector(query) - if (not skip_debug) and (debug or pykx_qdebug): + if (not skip_debug) and (debug or _get_qdebug()): if 0 != len(args): query = wrappers.List([query, *[wrappers.K(x) for x in args]]) result = _keval( diff --git a/src/pykx/exceptions.py b/src/pykx/exceptions.py index 50df431..c7ab89e 100644 --- a/src/pykx/exceptions.py +++ b/src/pykx/exceptions.py @@ -34,11 +34,12 @@ class PyKXException(Exception): class LicenseException(PyKXException): """Exception for when a feature that requires a valid q license is used without one.""" def __init__(self, feature_msg='use this feature', *args, **kwargs): + # Imported here rather than at module scope as pykx.config imports from this module + from .config import _get_unlicensed_reason super().__init__( - 'A valid q license must be in a known location (e.g. `$QLIC`) to ' - f'{feature_msg}.', + f'Cannot {feature_msg} as KDB-X Python is operating in unlicensed mode. {_get_unlicensed_reason()}', # noqa: E501 *args, - **kwargs + **kwargs, ) diff --git a/src/pykx/ipc.py b/src/pykx/ipc.py index 0ec0fa3..8ea2846 100644 --- a/src/pykx/ipc.py +++ b/src/pykx/ipc.py @@ -19,7 +19,7 @@ import sys from . import deserialize, serialize, Q -from .config import max_error_length, pykx_lib_dir, pykx_qdebug, system +from .config import _get_qdebug, max_error_length, pykx_lib_dir, system from .core import licensed from .exceptions import FutureCancelled, NoResults, PyKXException, QError, UninitializedConnection from .util import get_default_args, normalize_to_bytes, normalize_to_str @@ -327,7 +327,7 @@ def result(self) -> Any: if self._cancelled_message != '': print('Connection was lost no result', file=sys.stderr) return None - if self._debug or pykx_qdebug: + if self._debug or _get_qdebug(): if self._result._unlicensed_getitem(0).py() == True: print((self._result._unlicensed_getitem(1).py()).decode(), file=sys.stderr) raise QError(self._result._unlicensed_getitem(2).py().decode()) @@ -573,6 +573,8 @@ def _init(self, reconnection_function: callable = reconnection_function, connection_timeout: Optional[float] = None, ): + if self.__dict__.get('_initialized'): + return credentials = f'{normalize_to_str(username, "Username")}:' \ f'{normalize_to_str(password, "Password")}' object.__setattr__(self, '_connection_info', { @@ -696,7 +698,7 @@ def _send(self, if self.closed: raise RuntimeError(_ipc_err_warning['closed']) tquery = type(query) - debugging = (not skip_debug) and (debug or pykx_qdebug) + debugging = (not skip_debug) and (debug or _get_qdebug()) if issubclass(tquery, SymbolicFunction): if licensed: query = query.func @@ -1044,7 +1046,8 @@ def upd(self, table: str, data:Union[list, Table]) -> None: ```python >>> import pykx as kx >>> with kx.SyncQConnection(port=5050) as q: - ... q.upd('trade', [kx.TimespanAtom('now') 'AAPL', 1.0]) + ... q.upd('trade', [kx.TimespanAtom('now'), 'AAPL', 1.0]) + >>> N = 100 >>> trades = kx.Table(data = { ... 'time': kx.TimespanAtom('now'), ... 'sym': kx.random.random(N, ['AAPL', 'MSFT', 'GOOG']), @@ -1359,7 +1362,7 @@ def _call(self, if not wait: return K(None) res = self._recv(locked=True) - if skip_debug or not (debug or pykx_qdebug): + if skip_debug or not (debug or _get_qdebug()): return res if res._unlicensed_getitem(0).py() == True: print((res._unlicensed_getitem(1).py()).decode(), file=sys.stderr) @@ -1439,10 +1442,13 @@ def close(self) -> None: self._writer.close() try: self._sock.shutdown(socket.SHUT_RDWR) + except BaseException: + pass + try: self._sock.close() - self._finalizer() except BaseException: pass + self._finalizer() def fileno(self) -> int: """The file descriptor or handle of the connection.""" @@ -1948,10 +1954,13 @@ async def close(self) -> None: self._writer.close() try: self._sock.shutdown(socket.SHUT_RDWR) + except BaseException: + pass + try: self._sock.close() - self._finalizer() except BaseException: pass + self._finalizer() def fileno(self) -> int: """The file descriptor or handle of the connection.""" @@ -2020,10 +2029,13 @@ def close(self) -> None: self._writer.close() try: self._sock.shutdown(socket.SHUT_RDWR) + except BaseException: + pass + try: self._sock.close() - self._finalizer() except BaseException: pass + self._finalizer() def handshake(conn: socket.socket): @@ -2792,10 +2804,13 @@ async def close(self) -> None: self._writer.close() try: self._sock.shutdown(socket.SHUT_RDWR) + except BaseException: + pass + try: self._sock.close() - self._finalizer() except BaseException: pass + self._finalizer() class SecureQConnection(QConnection): @@ -3033,7 +3048,7 @@ def _call(self, try: with self._lock if self._lock is not None else nullcontext(): - if debug or pykx_qdebug: + if debug or _get_qdebug(): res = handler( handle, normalize_to_bytes( diff --git a/src/pykx/lib/.gitignore b/src/pykx/lib/.gitignore index 0af2287..77b1995 100644 --- a/src/pykx/lib/.gitignore +++ b/src/pykx/lib/.gitignore @@ -3,7 +3,7 @@ **/*q.so **/*q.dylib **/q.dll -**/q.lib +**/qdll.lib **/kxreaper **/libkurl.so **/libobjstor.so diff --git a/src/pykx/lib/kdbx-libs/.gitignore b/src/pykx/lib/kdbx-libs/.gitignore index 0af2287..77b1995 100644 --- a/src/pykx/lib/kdbx-libs/.gitignore +++ b/src/pykx/lib/kdbx-libs/.gitignore @@ -3,7 +3,7 @@ **/*q.so **/*q.dylib **/q.dll -**/q.lib +**/qdll.lib **/kxreaper **/libkurl.so **/libobjstor.so diff --git a/src/pykx/license.py b/src/pykx/license.py index be26faa..6d82608 100644 --- a/src/pykx/license.py +++ b/src/pykx/license.py @@ -153,7 +153,7 @@ def expires() -> int: def install(license: str, *, format: Optional[str] = 'FILE', - license_type: Optional[str] = 'kc.lic', + license_type: Optional[str] = None, force: Optional[bool] = False ) -> bool: """ @@ -164,9 +164,10 @@ def install(license: str, If "STRING" this is the base64 encoded string provided in your license email format: Is the license check being completed using a downloaded file or base64 encoded string. Accepted inputs are "FILE"(default) or "STRING". - license_type: The license file type/name which is to be checked, by default this - is 'kc.lic' but can be changed to 'k4.lic' or 'kx.lic' if appropriate - force: Enforce overwrite without opt-in message for overwrite + license_type: The license file type/name which is to be installed. If unspecified + and a "FILE" is being installed whose name is one of 'kc.lic', 'k4.lic' or + 'kx.lic', that name is used; otherwise it defaults to 'kc.lic'. + force: Enforce overwrite without opt-in message for overwrite. Returns: A boolean indicating if the license has been correctly overwritten @@ -194,6 +195,12 @@ def install(license: str, if format not in ('file', 'string'): raise Exception('Unsupported option provided for format parameter') + if license_type is None: + known_types = ('kc.lic', 'k4.lic', 'kx.lic') + incoming_name = (os.path.basename(os.path.expanduser(Path(license))) + if format == 'file' else None) + license_type = incoming_name if incoming_name in known_types else 'kc.lic' + license_located = False installed_lic = qlic/license_type if os.path.exists(installed_lic): @@ -210,7 +217,7 @@ def install(license: str, if not os.path.exists(download_location): raise Exception(f'Download location provided {download_location} does not exist.') - shutil.copy(download_location, qlic) + shutil.copy(download_location, qlic/license_type) else: try: lic = base64.b64decode(license) @@ -220,5 +227,5 @@ def install(license: str, with open(qlic/license_type, 'wb') as binary_file: binary_file.write(lic) - print("KDB-X Python license successfully installed!") + print(f"KDB-X Python license successfully installed: {qlic/license_type}") return True diff --git a/src/pykx/pandas_api/pandas_indexing.py b/src/pykx/pandas_api/pandas_indexing.py index 9c57129..19cd505 100644 --- a/src/pykx/pandas_api/pandas_indexing.py +++ b/src/pykx/pandas_api/pandas_indexing.py @@ -327,9 +327,7 @@ def _rename_columns(tab, labels): for x in list(labels.keys()): if type(labels[x]) is not str: raise ValueError('pykx.Table column names can only be of type pykx.SymbolAtom') - if type(x) is not str: - labels.pop(x) - if x not in tab.columns: + if type(x) is not str or x not in tab.columns: labels.pop(x) if "Keyed" in str(type(tab)): return q('''{ diff --git a/src/pykx/pandas_api/pandas_reset_index.py b/src/pykx/pandas_api/pandas_reset_index.py index 179d46f..b0b1cae 100644 --- a/src/pykx/pandas_api/pandas_reset_index.py +++ b/src/pykx/pandas_api/pandas_reset_index.py @@ -62,7 +62,7 @@ def reset_index(self, levels=None, *, drop=False, inplace=False, levels, self) if drop: - res = q('{x[y;(),cols[key y]z]]}', drop_keys, self, levels) # noqa: E501 + res = q('{x[y;(),cols[key y]z]}', drop_keys, self, levels) # noqa: E501 else: res = q('{(cols[key x] except cols[key x]y) xkey x}', self, levels) else: diff --git a/src/pykx/pykx.q b/src/pykx/pykx.q index 86266e7..1082151 100644 --- a/src/pykx/pykx.q +++ b/src/pykx/pykx.q @@ -14,6 +14,10 @@ if[`e in key`.p; ] ] +if["" ~ getenv`PYKX_LOADING_PYTHON; setenv[`PYKX_LOADING_Q; "true"]]; +if[not["" ~ getenv`PYKX_OLD_QHOME] and not["" ~ getenv`PYKX_LOADING_Q]; setenv[`QHOME; getenv`PYKX_OLD_QHOME]]; +if[not["" ~ getenv`PYKX_OLD_QPATH] and not["" ~ getenv`PYKX_LOADING_Q]; setenv[`QPATH; getenv`PYKX_OLD_QPATH]]; + // @private // @desc Process context prior to KDB-X Python initialization .pykx.util.prevCtx:system"d"; @@ -158,8 +162,17 @@ util.CFunctions:flip `qname`cname`args!flip ( util.toPython:{wrap util.pyForeign[$[type[x]in 104 105 112h;wrap[unwrap x]`;x];y;z]} // @private -// @desc Check if argument is of specified type -util.isch :{$[(104= type y);$[x~y[::]0;1b;0b];0b]} +// @desc +// Safely extract the pykx marker tag from a value, or ` if it is not a marker. +util.mtag :{$[104h<>type x;`; + 104h<>type v0:(value x)0;`; + 2>count vv:value v0;`; + enlist~first vv;vv 1; + `]} + +// @private +// @desc Check if argument is a pykx marker of the specified type +util.isch :{x~util.mtag y} // @private // @desc @@ -921,7 +934,6 @@ setdefault:{ x in enlist "default" ;"default"; '"unknown conversion type: ",x ]; - setenv[`PYKX_DEFAULT_CONVERSION;$[-10h=type util.defaultConv;enlist;]util.defaultConv]; } // @kind function @@ -1726,14 +1738,7 @@ qcallable:{$[util.isw x;wrap[unwrap[x]](<);util.isf x;wrap[x](<);'"Could not con // "Hello World" // ``` safeReimport:{[x] - reimporter:.pykx.import[`pykx][`:PyKXReimport][]; - envlist:reimporter[`:envlist]`; - envvals:getenv each envlist; - - reimporter[`:reset][]; r: @[{(0b;x y)}x;(::);{(1b;x)}]; - - setenv'[envlist;envvals]; $[r 0;{'x};::] r 1 } @@ -2016,3 +2021,13 @@ loadExtension:{[ext] system"d ",string .pykx.util.prevCtx; k).Q.pykxld:{x:("#!"~2#*x)_x:-1!'x;+(1+*:'i;)@"\n"/:'x i:(&|1^\|0N 0 1@"/ "?*:'(v:x i),'"/")_i:&~|':(b?-1)#b:+\-/x~\:/:+,"/\\"} + +if[not["" ~ getenv`PYKX_LOADING_Q]; + setenv[`PYKX_UNDER_Q; ""]; + setenv[`PYKX_UNDER_PYTHON; ""]; + setenv[`PYKX_Q_LOADED_MARKER; ""]; + setenv[`PYKX_LOADED_UNDER_Q; ""]; + setenv[`PYKX_LOADING_Q; ""]; + setenv[`PYKX_LOADING_PYTHON; ""]; + setenv[`PYKX_QINIT_CHECK; ""]; + ]; diff --git a/src/pykx/pykxq_m.c b/src/pykx/pykxq_m.c index 7f2d038..d8a1ba8 100644 --- a/src/pykx/pykxq_m.c +++ b/src/pykx/pykxq_m.c @@ -205,11 +205,8 @@ EXPORT K k_init_python(K x, K y, K z) { Py_DecRef(a); } PyErr_Clear(); - struct CallArgs* init_args = malloc(sizeof(struct CallArgs)); - init_args->arg1 = x; - init_args->arg2 = y; - init_args->arg3 = z; - pthread_create(&thread, NULL, thread_init, (void*)init_args); + // thread_init does not read its argument, so no allocation is required here. + pthread_create(&thread, NULL, thread_init, NULL); return (K)0; } diff --git a/src/pykx/q.so/libs/kdbx/l64/libq.so b/src/pykx/q.so/libs/kdbx/l64/libq.so index 99d779b..f0acedb 100755 Binary files a/src/pykx/q.so/libs/kdbx/l64/libq.so and b/src/pykx/q.so/libs/kdbx/l64/libq.so differ diff --git a/src/pykx/q.so/libs/kdbx/l64arm/libq.so b/src/pykx/q.so/libs/kdbx/l64arm/libq.so index d9515f0..b6ff66e 100755 Binary files a/src/pykx/q.so/libs/kdbx/l64arm/libq.so and b/src/pykx/q.so/libs/kdbx/l64arm/libq.so differ diff --git a/src/pykx/q.so/libs/kdbx/m64/libq.dylib b/src/pykx/q.so/libs/kdbx/m64/libq.dylib index 298295b..2527ddc 100755 Binary files a/src/pykx/q.so/libs/kdbx/m64/libq.dylib and b/src/pykx/q.so/libs/kdbx/m64/libq.dylib differ diff --git a/src/pykx/q.so/libs/kdbx/m64arm/libq.dylib b/src/pykx/q.so/libs/kdbx/m64arm/libq.dylib index e5f2278..20f73d0 100755 Binary files a/src/pykx/q.so/libs/kdbx/m64arm/libq.dylib and b/src/pykx/q.so/libs/kdbx/m64arm/libq.dylib differ diff --git a/src/pykx/q.so/libs/kdbx/w64/q.dll b/src/pykx/q.so/libs/kdbx/w64/q.dll index f0ad5d4..400d92e 100644 Binary files a/src/pykx/q.so/libs/kdbx/w64/q.dll and b/src/pykx/q.so/libs/kdbx/w64/q.dll differ diff --git a/src/pykx/q.so/libs/kdbx/w64/q.lib b/src/pykx/q.so/libs/kdbx/w64/q.lib deleted file mode 100644 index a4beb9d..0000000 Binary files a/src/pykx/q.so/libs/kdbx/w64/q.lib and /dev/null differ diff --git a/src/pykx/q.so/libs/kdbx/w64/qdll.lib b/src/pykx/q.so/libs/kdbx/w64/qdll.lib new file mode 100644 index 0000000..8a999ab Binary files /dev/null and b/src/pykx/q.so/libs/kdbx/w64/qdll.lib differ diff --git a/src/pykx/q.so/qk/pykx_init.q_ b/src/pykx/q.so/qk/pykx_init.q_ index f9e96c5..682c2bf 100644 Binary files a/src/pykx/q.so/qk/pykx_init.q_ and b/src/pykx/q.so/qk/pykx_init.q_ differ diff --git a/src/pykx/read.py b/src/pykx/read.py index ce5d4a5..9fe87a5 100644 --- a/src/pykx/read.py +++ b/src/pykx/read.py @@ -59,7 +59,7 @@ def update_types(self, types): upd_types.append(i) else: found = False - for key in _type_mapping.keys(): + for key in sorted(_type_mapping.keys(), key=len, reverse=True): if key in t: found = True upd_types.append(_type_mapping[key]) @@ -278,8 +278,8 @@ def fixed(self, Examples: - Read a file of fixed width data into a `pykx.List` of two `pykx.LongVectors` the first - with a size of 1 character and the second with a size of 2 characters. + Read a file of fixed width data into a `pykx.List` of two `pykx.LongVector` objects, + the first with a size of 1 character and the second with a size of 2 characters. ```python data = q.read.fixed('example_file', [b'J', b'J'], [1, 2]) diff --git a/src/pykx/register.py b/src/pykx/register.py index c60c6ae..00a01c8 100644 --- a/src/pykx/register.py +++ b/src/pykx/register.py @@ -73,7 +73,7 @@ def py_toq(py_type: Any, ```python >>> def complex_toq_upd(data): - ... return kx.q('{`real`imag!(x;y)}', kx.toq(data.real), kx.toq(data.imag) + ... return kx.q('{`real`imag!(x;y)}', kx.toq(data.real), kx.toq(data.imag)) >>> kx.register.py_toq(complex, complex_toq_upd, overwrite=True) >>> kx.toq(complex(1, 2)) pykx.Dictionary(pykx.q(' @@ -138,7 +138,7 @@ def column_function(name: str, >>> def min_max_scaler(self): ... return self.call('{(x-minData)%max[x]-minData:min x}') >>> kx.register.column_function('minmax', min_max_scaler) - >>> tab.select(kx.Column('true') & kx.Column('true').minmax().rename('scaled_true')) + >>> tab.select(kx.Column('true') & kx.Column('true').minmax().name('scaled_true')) ``` Register mean-absolute error function to be applied between 'true' and 'pred' columns diff --git a/src/pykx/reimporter.py b/src/pykx/reimporter.py index 4b17e86..f4374f9 100644 --- a/src/pykx/reimporter.py +++ b/src/pykx/reimporter.py @@ -9,8 +9,6 @@ """ import os -from .config import pykx_executable, qhome - class PyKXReimport: """Helper class to help manage the environment variables around reimporting KDB-X Python in a @@ -27,19 +25,23 @@ class PyKXReimport: # This process can safely import KDB-X Python subprocess.Popen(f"python other_file.py") ``` + + Note: Not required in most cases. + KDB-X Python now automatically manages these environment variables, so this helper is not + needed for the majority of use cases (for example spawning a Python subprocess, or a + KDB-X 5.0 q process). + + Note: Still required for spawning kdb+ 4.0/4.1 q subprocesses from Python. + When KDB-X Python is imported in a Python process it redirects `QHOME` to its internal + directory. A kdb+ 4.0/4.1 `q` child inheriting that `QHOME` fails to start because the + pykx-internal `QHOME` does not contain `q.k`. Wrapping the spawn in `PyKXReimport` restores + the user's `QHOME` (which contains `q.k`) for the child. This also applies where the QHOME + symlinks pykx creates are unavailable or unreliable (e.g. Windows). Alternatively set + `PYKX_IGNORE_QHOME=true` so `QHOME` is not redirected in the first place. """ def __init__(self): - self.envlist = ('PYKX_DEFAULT_CONVERSION', - 'PYKX_UNDER_Q', - 'PYKX_UNDER_PYTHON', - 'PYKX_SKIP_UNDERQ', - 'PYKX_Q_LOADED_MARKER', - 'PYKX_LOADED_UNDER_Q', - 'QHOME', - 'PYKX_EXECUTABLE', - 'PYKX_DIR') - self.envvals = [os.getenv(x) for x in self.envlist] + self.pykx_qhome = os.getenv('QHOME', '') def __enter__(self): self.reset() @@ -51,12 +53,7 @@ def reset(self): Note: It is not recommended to use this function directly instead use the `with` syntax. This will automatically manage setting and restoring the environment variables for you. """ - for x, y in zip(self.envlist, self.envvals): - os.unsetenv(x) - if y is not None: - del os.environ[x] - os.environ['QHOME'] = str(qhome) - os.environ['PYKX_EXECUTABLE'] = pykx_executable + os.environ['QHOME'] = os.getenv('PYKX_OLD_QHOME', self.pykx_qhome) def restore(self): """Restore all the required environment variables. @@ -64,12 +61,13 @@ def restore(self): Note: It is not recommended to use this function directly instead use the `with` syntax. This will automatically manage setting and restoring the environment variables for you. """ - for x, y in zip(self.envlist, self.envvals): - if y is not None: - os.environ[x] = y + os.environ['QHOME'] = self.pykx_qhome def __exit__(self, exc_type, exc_value, exc_tb): self.restore() def __del__(self): - self.restore() + # Best-effort restore; `__init__` may not have completed (e.g. QHOME unset). + qhome = getattr(self, 'pykx_qhome', None) + if qhome is not None: + os.environ['QHOME'] = qhome diff --git a/src/pykx/schema.py b/src/pykx/schema.py index d13963e..fb2ccec 100644 --- a/src/pykx/schema.py +++ b/src/pykx/schema.py @@ -40,7 +40,7 @@ def __dir__(): k.DatetimeAtom: "datetime", k.DatetimeVector: "datetime", k.TimespanAtom: "timespan", k.TimespanVector: "timespan", k.MinuteAtom: "minute", k.MinuteVector: "minute", - k.SecondAtom: "second", k.SecondVector: "sector", + k.SecondAtom: "second", k.SecondVector: "second", k.TimeAtom: "time", k.TimeVector: "time", } diff --git a/src/pykx/streamlit.py b/src/pykx/streamlit.py index c415bfe..0f42689 100644 --- a/src/pykx/streamlit.py +++ b/src/pykx/streamlit.py @@ -167,7 +167,7 @@ def query(self, query: str, *args, format='q', **kwargs): Parameters: query: A q expression to be evaluated. This must be valid q, qSQL or SQL in the KX Insights style. - *args: Arguments to the query. Each argument will be converted into a `#!python pykx.K` + *args: Arguments to the query. Each argument will be converted into a `#!python pykx` object. Up to 8 arguments can be provided (maximum supported by q functions). format: Description of query format for internal pre-processing before the query is sent to the server. This must be one of 'q', 'qsql' or 'sql'. diff --git a/src/pykx/toq.pyx b/src/pykx/toq.pyx index d8a2937..3ae5c7d 100644 --- a/src/pykx/toq.pyx +++ b/src/pykx/toq.pyx @@ -112,7 +112,7 @@ from . import wrappers as k from ._pyarrow import pyarrow as pa from .cast import * from . import config -from .config import beta_features, find_core_lib, k_allocator, licensed, pandas_gt1, system +from .config import beta_features, find_core_lib, keep_local_times, k_allocator, licensed, pandas_gt1, system from .constants import NULL_INT16, NULL_INT32, NULL_INT64 from .constants import INF_INT16, INF_INT32, INF_INT64, INF_NEG_INT16, INF_NEG_INT32, INF_NEG_INT64 from .exceptions import LicenseException, PyArrowUnavailable, PyKXException, QError @@ -120,6 +120,12 @@ from .util import df_from_arrays, slice_to_range import importlib.util _torch_unavailable = importlib.util.find_spec('torch') is None +_np_get_handler_name = (np.core.multiarray if np.__version__[0] == '1' + else np._core.multiarray).get_handler_name + + +cdef bint _pykx_allocated(x): + return _np_get_handler_name(x) == 'pykx_allocator' __all__ = [ @@ -971,9 +977,7 @@ def from_list(x: list, if ktype is not None and not ktype is k.List: try: np_type = pykx_ktype_to_np_type.get(ktype) if ktype != k.GUIDVector else object - if ktype is k.TimestampVector and config.keep_local_times: - x = [y.replace(tzinfo=None) for y in x] return from_numpy_ndarray(np.array(x, dtype=np_type), ktype, cast=cast, handle_nulls=handle_nulls, strings_as_char=strings_as_char, no_allocator=no_allocator) except TypeError as ex: @@ -1379,11 +1383,19 @@ def from_numpy_ndarray(x: np.ndarray, Returns: An instance of a subclass of `pykx.Vector`. """ - owndata = False - writeable = False + # Structured arrays (those with named fields) map onto q tables rather than vectors. + if x.dtype.names is not None: + return from_numpy_recarray(x, ktype, cast=cast, handle_nulls=handle_nulls, strings_as_char=strings_as_char, no_allocator=no_allocator) + skip_allocator = False if hasattr(x, 'flags'): - owndata = x.flags.owndata - writeable = x.flags.writeable + if ( + not k_allocator + or no_allocator + or not x.flags.owndata + or not x.flags.writeable + or not _pykx_allocated(x) + ): + skip_allocator = True if str(x.dtype) == "pykx.uuid": x = x.array @@ -1483,7 +1495,7 @@ def from_numpy_ndarray(x: np.ndarray, core.r0(kx) raise TypeError('Item size mismatch when converting Numpy ndarray to q: q item size ' f'({itemsize}) != Numpy item size ({x.itemsize})') - if not k_allocator or no_allocator or not owndata or not writeable: + if skip_allocator: kx = core.ktn(ktype.t, n) data = x.__array_interface__['data'][0] memcpy( kx.G0, data, n * itemsize) @@ -1536,12 +1548,12 @@ def from_numpy_ndarray(x: np.ndarray, core.r0(kx) raise TypeError('Item size mismatch when converting Numpy ndarray to q: q item size ' f'({itemsize}) != Numpy item size ({x.itemsize})') - if not k_allocator or no_allocator or not owndata or not writeable: + if skip_allocator: kx = core.ktn(ktype.t, n) data = x.__array_interface__['data'][0] memcpy( kx.G0, data, n * itemsize) return factory(kx, False) - if not k_allocator or no_allocator or not owndata or not writeable: + if skip_allocator: return factory(kx, False) # nocov Py_INCREF(x) data = x.__array_interface__['data'][0] @@ -1553,6 +1565,100 @@ def from_numpy_ndarray(x: np.ndarray, return res +def from_numpy_recarray(x: np.recarray, + ktype: Optional[KType] = None, + *, + cast: bool = False, + handle_nulls: bool = False, + strings_as_char: bool = False, + no_allocator: bool = False, +) -> Union[k.Table, k.KeyedTable]: + """Converts a structured `numpy.ndarray` (including a `numpy.recarray`) into a `pykx.Table`. + + This is the inverse of [`pykx.Table.np`][pykx.wrappers.Table.np], which produces a + `numpy.recarray` via `pandas.DataFrame.to_records`. Each named field of the array's dtype + becomes a column of the resulting q table, with each field converted independently as a + `numpy.ndarray` would be. + + See Also: + - [`from_numpy_ndarray`][pykx.toq.from_numpy_ndarray] + - [`from_pandas_dataframe`][pykx.toq.from_pandas_dataframe] + + Parameters: + x: The structured `numpy.ndarray`/`numpy.recarray` that will be converted into a q table. + Its dtype must have named fields (i.e. `x.dtype.names is not None`). + ktype: Desired `pykx.K` subclass (or type number) for the returned value. If `None`, a + `pykx.Table` is returned. The following values are supported: + + +-------------------+-----------------------------------------------------------------+ + | `ktype` | Returned value | + +===================+=================================================================+ + | `None` | Same as for `ktype=pykx.Table`. | + +-------------------+-----------------------------------------------------------------+ + | `pykx.Table` | The record array as a q table. | + +-------------------+-----------------------------------------------------------------+ + | `pykx.KeyedTable` | The record array as a q keyed table. | + +-------------------+-----------------------------------------------------------------+ + cast: Unused. + handle_nulls: Convert `pd.NaT` to corresponding q null values in Pandas dataframes and + Numpy arrays. + + Raises: + TypeError: The array is not a structured array, has more than one dimension, or the + `ktype` is unsupported. + + Returns: + An instance of `pykx.Table` or `pykx.KeyedTable`. + """ + cdef core.K kx + if getattr(x, 'dtype', None) is None or x.dtype.names is None: + raise _conversion_TypeError(x, repr('numpy.recarray'), ktype) + if x.ndim != 1: + raise TypeError( + 'Only one dimensional record arrays can be converted to a q table, received an ' + f'array with {x.ndim} dimensions' + ) + # Keyed tables (and any other non-default target) are handled by the more general Pandas + # path, which understands how to build the key columns from an index. + if not (ktype is None or ktype is k.Table): + return from_pandas_dataframe( + pd.DataFrame.from_records(x), + ktype, + cast=cast, + handle_nulls=handle_nulls, + strings_as_char=strings_as_char, + no_allocator=no_allocator, + ) + # Convert each field independently as a numpy array, preserving its dtype (e.g. an 'S1' + # field becomes a char column and a 'U1' field a symbol column) and assemble the columns + # into a table. + columns = {} + for name in x.dtype.names: + col = from_numpy_ndarray( + x[name], + cast=cast, + handle_nulls=handle_nulls, + strings_as_char=strings_as_char, + no_allocator=no_allocator, + ) + # A table column must be a vector. A single element field (e.g. a one row 'S1'/bytes + # column) converts to an atom, so enlist it into a length one vector. + if isinstance(col, k.Atom): + col = from_list([col]) + columns[name] = col + kk = from_dict( + columns, + cast=cast, + handle_nulls=handle_nulls, + strings_as_char=strings_as_char, + no_allocator=no_allocator, + ) + kx = core.xT(core.r1(_k(kk))) + if kx == NULL: + raise PyKXException('Failed to create table from numpy record array') + return factory(kx, False) + + _size_to_nan = { 2: NULL_INT16, 4: NULL_INT32, @@ -1577,6 +1683,8 @@ def _to_numpy_or_categorical(x, col_name=None, df=None): if isinstance(x, np.ndarray): return x elif isinstance(x, (pd.Series, pd.Index)): + if keep_local_times and hasattr(x, 'dt') and hasattr(x.dt, 'tz_localize'): + x = x.dt.tz_localize(None) if isinstance(x.values, pd.Categorical): return from_pandas_categorical( x.values, @@ -1584,8 +1692,12 @@ def _to_numpy_or_categorical(x, col_name=None, df=None): ) elif isinstance(x.values, pd.core.arrays.ExtensionArray): attrnull = hasattr(x, 'isnull') - hasnull = x.isnull().values.any() if attrnull else False - if isinstance(x.dtype, pd.StringDtype) and attrnull and hasnull: + isnull = x.isnull().values if attrnull else None + hasnull = isnull.any() if attrnull else False + string_dtype = pd.api.types.is_string_dtype(x.dtype) + if string_dtype and (len(x) == 0 or (attrnull and isnull.all())): + return np.full(len(x), '', dtype='U1') + elif string_dtype and attrnull and hasnull: return np.array(x.to_numpy(copy=False, na_value=None)) elif x.dtype.kind != 'f' and attrnull and hasnull: if not x.dtype.kind in ['i', 'M', 'm']: @@ -2279,6 +2391,8 @@ def from_numpy_datetime64(x: np.datetime64, """ cdef core.K kx if isinstance(x, pd._libs.tslibs.timestamps.Timestamp): + if keep_local_times and hasattr(x, 'tz_localize'): + x = x.tz_localize(None) x = x.to_datetime64() if ktype is None or ktype is k.TimestampAtom: kx = core.ktj(-12, x.astype(np.dtype('datetime64[ns]')).astype(np.int64) - TIMESTAMP_OFFSET) # noqa @@ -2597,7 +2711,6 @@ def from_ellipsis(x: Ellipsis, return q('value[(;)]1') -# TODO: After Python 3.7 support is dropped, use a `typing.Protocol` for `x` (KXI-9158) def from_fileno(x: Any, ktype: Optional[KType] = None, *, @@ -2925,6 +3038,7 @@ _converter_from_python_type = { np.ndarray: from_numpy_ndarray, np.ma.MaskedArray: from_numpy_ndarray, + np.recarray: from_numpy_recarray, pd.DataFrame: from_pandas_dataframe, pd.Series: from_pandas_series, diff --git a/src/pykx/util.py b/src/pykx/util.py index b9f6a7d..882f7fa 100644 --- a/src/pykx/util.py +++ b/src/pykx/util.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from functools import wraps +from functools import cached_property, wraps import inspect import io import os @@ -15,18 +15,23 @@ from warnings import warn import pandas as pd -from pandas.core.internals import BlockManager, make_block +from pandas.core.internals import BlockManager import numpy as np import requests import toml from .config import ( - _executable, _get_qexecutable, _get_qhome, + _executable, _get_qdebug, _get_qexecutable, _get_qhome, _set_qdebug, beta_features, ignore_qhome, jupyterq, k_allocator, k_gc, keep_local_times, - load_pyarrow_unsafe, max_error_length, no_pykx_signal, numpy_gt1, pandas_gt2, pykx_config_location, # noqa E501 - pykx_config_profile, pykx_debug_insights, pykx_dir, pykx_lib_dir, pykx_profile_content, - pykx_qdebug, pykx_threading, q_executable, qargs, qce, qhome, qlic, release_gil, - skip_under_q, suppress_warnings, use_q_lock) + load_pyarrow_unsafe, max_error_length, no_pykx_signal, numpy_gt1, pandas_gt2, # noqa E501 + pykx_config_location, pykx_config_profile, pykx_debug_insights, pykx_dir, pykx_lib_dir, + pykx_profile_content, pykx_threading, q_executable, qargs, qce, qhome, qlic, + release_gil, skip_under_q, suppress_warnings, use_q_lock) + +if pandas_gt2: + from pandas.api.internals import create_dataframe_from_blocks +else: + from pandas.core.internals import make_block from ._version import version as __version__ from .exceptions import PyKXException @@ -95,22 +100,6 @@ def num_available_cores() -> int: return os.cpu_count() # nocov -# TODO: Once Python 3.7 support is dropped, we can replace this with `functools.cached_property` -class cached_property: - """Property-like descriptor which overwrites itself with the first obtained property value.""" - def __init__(self, func): - self.func = func - self.attrname = func.__name__ - self.__doc__ = func.__doc__ - - def __get__(self, instance, owner=None): - if instance is None: - return self - value = self.func(instance) - instance.__dict__[self.attrname] = value - return value - - class class_or_instancemethod(classmethod): def __get__(self, instance, type_): descr_get = super().__get__ if instance is None else self.__func__.__get__ @@ -274,12 +263,18 @@ def df_from_arrays(columns, arrays, index): df = pd.DataFrame(index=index) for col, arr in zip(columns, arrays): df[col] = arr + elif pandas_gt2: + blocks = [ + (a.reshape((1, len(a))), np.array([i])) + for i, a in enumerate(arrays) + ] + df = create_dataframe_from_blocks(blocks, index=index, columns=columns) else: blocks = tuple( make_block(values=a.reshape((1, len(a))), placement=(i,)) for i, a in enumerate(arrays) ) - df= pd.DataFrame( + df = pd.DataFrame( BlockManagerUnconsolidated(axes=[columns, index], blocks=blocks), copy=False ) @@ -480,7 +475,7 @@ def env_information(): 'PYKX_RELEASE_GIL': release_gil, 'PYKX_Q_LIB_LOCATION': pykx_lib_dir, 'PYKX_Q_LOCK': use_q_lock, 'PYKX_SKIP_UNDERQ': skip_under_q, 'PYKX_Q_EXECUTABLE': q_executable, 'PYKX_THREADING': pykx_threading, - 'PYKX_QDEBUG': pykx_qdebug, + 'PYKX_QDEBUG': _get_qdebug(), 'PYKX_DEBUG_INSIGHTS_LIBRARIES': pykx_debug_insights, 'PYKX_CONFIGURATION_LOCATION': pykx_config_location, 'PYKX_NO_SIGNAL': no_pykx_signal, @@ -726,20 +721,21 @@ def start_q_subprocess(port: int, 'Please install q using the function "kx.util.install_q" or following the ' 'instructions at:\nhttps://code.kx.com/pykx/getting-started/installing.html' ) - with PyKXReimport(): - qinit = [q_executable, load_file, '-p', f'{port}'] - if init_args is not None: - if not isinstance(init_args, list): - raise TypeError('Supplied additional startup arguments must be a list') - if not all(isinstance(s, str) for s in init_args): - raise TypeError('All supplied arguments to init_args must be str type objects') - qinit.extend(init_args) - server = subprocess.Popen( - qinit, - stdin=subprocess.PIPE, - stdout=None if process_logs else subprocess.DEVNULL, - stderr=None) - time.sleep(2) + qinit = [q_executable, load_file, '-p', f'{port}'] + if init_args is not None: + if not isinstance(init_args, list): + raise TypeError('Supplied additional startup arguments must be a list') + if not all(isinstance(s, str) for s in init_args): + raise TypeError('All supplied arguments to init_args must be str type objects') + qinit.extend(init_args) + server = subprocess.Popen( + qinit, + stdin=subprocess.PIPE, + stdout=None if process_logs else subprocess.DEVNULL, + stderr=None, + env={**os.environ, 'QHOME': os.getenv('PYKX_OLD_QHOME', '')}, + ) + time.sleep(2) return server @@ -826,3 +822,16 @@ def delete_q_variable(variable: str, namespace: str = '', garbage_collect: bool q('{![x;();0b;enlist y]}', ns, variable) if garbage_collect: q.Q.gc() + + +def qdebug(enable: bool = True): + """ + Enables or disables q debugging at runtime. + + Parameters: + enable: If True enables q debug mode. + + Returns: + None + """ + _set_qdebug(enable) diff --git a/src/pykx/wrappers.py b/src/pykx/wrappers.py index fd1cb4e..d467e2b 100644 --- a/src/pykx/wrappers.py +++ b/src/pykx/wrappers.py @@ -250,6 +250,22 @@ def _rich_convert(x: 'K', stdlib: bool = True, raw=False): return x.np(raw=raw) +def _as_array(arr, dtype, copy): + """Apply the NumPy `__array__(dtype, copy)` protocol to an already-materialised array. + + NumPy 2.* passes `dtype` positionally and `copy` by keyword to `__array__`, so every + implementation must accept both. A dtype conversion is unavoidably a copy, so an explicit + `copy=False` alongside a differing dtype is refused (matching `numpy.ndarray`). + """ + if dtype is not None and dtype != arr.dtype: + if copy is False: + raise ValueError('Unable to avoid copy while creating an array as requested.') + return arr.astype(dtype) + if copy: + return arr.copy() + return arr + + # HACK: This gets overwritten by the toq module to avoid a circular import error. def toq(*args, **kwargs): # nocov raise NotImplementedError @@ -598,8 +614,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): res = res._unlicensed_getitem(0) return res - def __array__(self, dtype=None): - return np.asarray(self.np(), dtype=dtype) + def __array__(self, dtype=None, copy=None): + return _as_array(np.asarray(self.np()), dtype, copy) class EnumAtom(Atom): @@ -657,9 +673,11 @@ def py(self, *, raw: bool = False, has_nulls: Optional[bool] = None, stdlib: boo as_np_timedelta = self.np() if pd.isnull(as_np_timedelta): return pd.NaT - return timedelta( - microseconds=int(as_np_timedelta.astype('timedelta64[ns]').astype(np.int64))//1000 - ) + if self._np_type == 'ns': + microseconds = int(as_np_timedelta.astype(np.int64)) // 1000 + else: + microseconds = int(as_np_timedelta.astype('timedelta64[us]').astype(np.int64)) + return timedelta(microseconds=microseconds) def np(self, *, @@ -1356,11 +1374,11 @@ def is_inf(self) -> bool: @property def is_pos_inf(self) -> bool: - return np.isposinf(self.py()) + return bool(np.isposinf(self.py())) @property def is_neg_inf(self) -> bool: - return np.isneginf(self.py()) + return bool(np.isneginf(self.py())) def __round__(self, ndigits=None): return round(self.py(), ndigits) @@ -1689,7 +1707,7 @@ def inf_neg(cls): # noqa: B902 @property def is_null(self) -> bool: - return self.py(raw=True) == 0j + return bool(self.py(raw=True) == 0j) @property def is_inf(self) -> bool: @@ -1873,13 +1891,13 @@ def __setitem__(self, key, val): raise QError(str(err)) self.__dict__.update(update_assign.__dict__) - def __array__(self): + def __array__(self, dtype=None, copy=None): # The `__array__` method must return a `np.ndarray`, not a `np.ma.masked_array`. As a # result, the null check we currently perform (by default) is a waste of time and memory, # since what will be returned in the end will ultimately expose the underlying values of # the nulls anyway. We can’t stop `__array__` from returning the raw null values, but we # can save that time and memory by using `has_nulls=False`. - return self.np(has_nulls=False) + return _as_array(self.np(has_nulls=False), dtype, copy) def __arrow_array__(self, type=None): if pa is None: @@ -2420,12 +2438,12 @@ class IntegralNumericVector(NumericVector): """Base type for all q integral numeric vectors.""" @property def has_nulls(self) -> bool: - return (self._base_null_value == self.np(raw=True)).any() + return bool((self._base_null_value == self.np(raw=True)).any()) @property def has_infs(self) -> bool: a = self.np(raw=True) - return (a == self._base_inf_value).any() or (a == -self._base_inf_value).any() + return bool((a == self._base_inf_value).any() or (a == -self._base_inf_value).any()) def py(self, *, raw: bool = False, has_nulls: Optional[bool] = None, stdlib: bool = True): if raw: @@ -2560,8 +2578,8 @@ def __len__(self): def __eq__(self, other): return self.array == other - def __array__(self): - return self.array + def __array__(self, dtype=None, copy=None): + return _as_array(self.array, dtype, copy) def reshape(self, *args, **kwargs): return self @@ -2619,7 +2637,7 @@ def _unlicensed_getitem(self, index): @property def has_nulls(self) -> bool: - return (0j == self.np(raw=True)).any() + return bool((0j == self.np(raw=True)).any()) @property def has_infs(self) -> bool: @@ -2718,11 +2736,12 @@ class NonIntegralNumericVector(NumericVector): @property def has_nulls(self) -> bool: a = self.np() - return np.isnan(np.dot(a, a)) # `np.dot` can be used as a high-performance NaN check + # `np.dot` can be used as a high-performance NaN check + return bool(np.isnan(np.dot(a, a))) @property def has_infs(self) -> bool: - return (self.np() == np.inf).any() + return bool(np.isinf(self.np()).any()) def __pow__(self, other, mod=None): if mod is None: @@ -2825,12 +2844,12 @@ class TemporalVector(Vector): """Base type for all q temporal vectors.""" @property def has_nulls(self) -> bool: - return (self._base_null_value == self.np(raw=True)).any() + return bool((self._base_null_value == self.np(raw=True)).any()) @property def has_infs(self) -> bool: a = self.np(raw=True) - return (a == self._base_inf_value).any() or (a == -self._base_inf_value).any() + return bool((a == self._base_inf_value).any() or (a == -self._base_inf_value).any()) def py(self, *, raw: bool = False, has_nulls: Optional[bool] = None, stdlib: bool = True): if raw: @@ -2936,19 +2955,17 @@ def py(self, if raw: return self.np(raw=True, has_nulls=has_nulls).tolist() if tzinfo is not None: + converted = self.np().astype('datetime64[us]').astype(datetime).tolist() if tzshift: - return [x.replace(tzinfo=pytz.utc).astimezone(tzinfo) - for x in self.np().astype('datetime64[us]').astype(datetime).tolist()] + return [pd.NaT if x is None else x.replace(tzinfo=pytz.utc).astimezone(tzinfo) + for x in converted] else: - return [x.replace(tzinfo=tzinfo) - for x in self.np().astype('datetime64[us]').astype(datetime).tolist()] - converted_vector=self.np().astype('datetime64[us]').astype(datetime).tolist() - null_pos=[] - for x in converted_vector: + return [pd.NaT if x is None else x.replace(tzinfo=tzinfo) + for x in converted] + converted_vector = self.np().astype('datetime64[us]').astype(datetime).tolist() + for i, x in enumerate(converted_vector): if x is None: - null_pos.append(converted_vector.index(x)) - for i in null_pos: - converted_vector[i]=pd.NaT + converted_vector[i] = pd.NaT return converted_vector @@ -3748,7 +3765,7 @@ def delete(self, columns=None, where=None, inplace=False): ```python >>> qtab.delete(kx.Column('age')) - >>> qtab.delete([kx.Column('age'), kx.Column('eye')]) + >>> qtab.delete(kx.Column('age') & kx.Column('eye')) ``` Delete rows of the dataset based on where condition @@ -3991,11 +4008,11 @@ def window_join(self, table, windows, cols, aggs): ... {'ask_max': [lambda x: max(x), 'ask'], ... 'ask_minus_bid': [lambda x, y: x - y, 'ask', 'bid']}) pykx.Table(pykx.q(' - sym time price ask_minus_bid ask_max + sym time price ask_max ask_minus_bid ---------------------------------------- - ibm 10:01:01 100 3 4 103 - ibm 10:01:04 101 4 1 1 1 104 - ibm 10:01:08 105 3 2 1 1 108 + ibm 10:01:01 100 103 3 4 + ibm 10:01:04 101 104 4 1 1 1 + ibm 10:01:08 105 108 3 2 1 1 ')) ``` """ @@ -4302,14 +4319,28 @@ def xbar(self, values): raise AttributeError("Operation 'xbar' not supported for PartitionedTable type") -class VirtualTable(Atom): +class VirtualTable(K): """Wrapper for q virtual tables.""" t = 112 + is_atom = False select = Table.select exec = Table.exec +_VT_base_error = "pykx.VirtualTable objects cannot be operated on directly, you must .select() or .exec() data from them first" # noqa: E501 +_VT_include = {'__module__', '__firstlineno__', '__doc__', '__init__', '__new__', '__del__', '_from_addr', '__repr__', '__str__'} # noqa: E501 + + +def _VT_base_error_method(self, *args, **kwargs): + raise QError(_VT_base_error) + + +for _name in vars(K): + if callable(getattr(K, _name)) and _name not in vars(VirtualTable)and _name not in _VT_include: + setattr(VirtualTable, _name, _VT_base_error_method) + + class Dictionary(Mapping): """Wrapper for q dictionaries, including regular dictionaries, and keyed tables.""" t = 99 @@ -5200,19 +5231,25 @@ def scan(self): def each_prior(self): return q("{x':}", self) - prior = each_prior + @property + def prior(self): + return self.each_prior @cached_property def each_right(self): return q('{x/:}', self) - sv = each_right + @property + def sv(self): + return self.each_right @cached_property def each_left(self): return q('{x\\:}', self) - vs = each_left + @property + def vs(self): + return self.each_left class Lambda(Function): @@ -6673,7 +6710,7 @@ def count(self, iterator=None): ... 'a': [1, -1, 0], ... 'b': [[-1, 2, 1], [0, 2], 1] ... }) - >>> tab.exec(kx.Column('b').count(iterator='each'))) + >>> tab.exec(kx.Column('b').count(iterator='each')) pykx.LongVector(pykx.q('3 2 1')) ``` """ @@ -8122,7 +8159,7 @@ def min(self, iterator=None): ... 'a': kx.random.random(100, 5), ... 'b': kx.random.random([100, 3], 5) ... }) - >>> tab.exec(kx.Column('a').max()) + >>> tab.exec(kx.Column('a').min()) pykx.LongAtom(pykx.q('0')) ``` @@ -8607,7 +8644,6 @@ def ratios(self, iterator=None): ```python >>> import pykx as kx >>> tab = kx.Table(data={ - >>> tab = kx.Table(data={ ... 'a': kx.random.random(100, 1000), ... 'b': kx.random.random([100, 3], 5) ... }) @@ -8632,7 +8668,6 @@ def reciprocal(self, iterator=None): ```python >>> import pykx as kx >>> tab = kx.Table(data={ - >>> tab = kx.Table(data={ ... 'a': kx.random.random(100, 1000), ... 'b': kx.random.random([100, 3], 5) ... }) @@ -8657,7 +8692,6 @@ def reverse(self, iterator=None): ```python >>> import pykx as kx >>> tab = kx.Table(data={ - >>> tab = kx.Table(data={ ... 'a': kx.q.til(100), ... 'b': kx.random.random([100, 3], 5) ... }) @@ -8759,13 +8793,22 @@ def rtrim(self, iterator=None): """ return self.call('rtrim', iterator=iterator) - def scov(self, iterator=None): + def scov(self, other, iterator=None, col_arg_ind=0, project_args=None): """ - Calculate the sample covariance for items in a column or rows in a column + Calculate the sample covariance between a column and one of: + + - Another column + - A Python list/numpy array + - A `pykx` variable in q memory Parameters: + other: The second column or variable (Python/q) to be used. iterator: What iterator to use when operating on the column for example, to execute per row, use `each` + col_arg_ind: Determines the index within the multivariate function + where the column parameter will be used. Default 0. + project_args: The argument indices of a multivariate function which will be + projected on the function before evocation with use of an iterator. Examples: @@ -8775,10 +8818,10 @@ def scov(self, iterator=None): >>> import pykx as kx >>> tab = kx.Table(data={ ... 'a': kx.random.random(100, 10.0), - ... 'b': kx.random.random([100, 3], 10.0) + ... 'b': kx.random.random(100, 10.0) ... }) - >>> tab.exec(kx.Column('a').scov()) - pykx.FloatAtom(pykx.q('8.983196')) + >>> tab.exec(kx.Column('a').scov(kx.Column('b'))) + pykx.FloatAtom(pykx.q('1.442801')) ``` Calculate the sample covariance for each row in a column: @@ -8786,22 +8829,23 @@ def scov(self, iterator=None): ```python >>> import pykx as kx >>> tab = kx.Table(data={ - ... 'a': kx.random.random(100, 10.0), - ... 'b': kx.random.random([100, 3], 10.0) + ... 'b': kx.random.random([100, 3], 10.0), + ... 'c': kx.random.random([100, 3], 10.0) ... }) - >>> tab.select(kx.Column('b').scov(iterator='each')) + >>> tab.select(kx.Column('b').scov(kx.Column('c'), iterator="'")) pykx.Table(pykx.q(' b - --------- - 0.3333333 - 0.3333333 - 5.333333 - 1.333333 + ----------- + -0.08110428 + -1.384917 + 0.7203654 + 0.3846634 .. ')) ``` """ - return self.call('scov', iterator=iterator) + return self.call('scov', other, iterator=iterator, col_arg_ind=col_arg_ind, + project_args=project_args) def sdev(self, iterator=None): """ @@ -9458,7 +9502,15 @@ def within(self, lower, upper, iterator=None, col_arg_ind=0, project_args=None): ... 'b': kx.q.desc(kx.random.random(100, 10)) ... }) >>> tab.select(where=kx.Column('a').within(today - 5, today - 3)) - pykx.FloatAtom(pykx.q('2.431111')) + pykx.Table(pykx.q(' + a b + ------------ + 2026.07.11 9 + 2026.07.12 9 + 2026.07.11 8 + 2026.07.12 8 + .. + ')) ``` """ return self.call('within', [lower, upper], iterator=iterator, col_arg_ind=col_arg_ind, @@ -10111,21 +10163,21 @@ def cast(self, other, iterator=None, col_arg_ind=1, project_args=None): ... }) >>> tab.dtypes pykx.Table(pykx.q(' - columns datatypes type - ----------------------------------- - a "kx.LongAtom" "kx.LongAtom" - b "kx.LongAtom" "kx.LongAtom" + columns datatypes + --------------------- + a "kx.LongAtom" + b "kx.LongAtom" ')) >>> tab.select( ... kx.Column('a') & ... kx.Column('a').cast('float').name('a_float') & ... kx.Column('b')).dtypes pykx.Table(pykx.q(' - columns datatypes type - ------------------------------------- - a "kx.LongAtom" "kx.LongAtom" - a_float "kx.FloatAtom" "kx.FloatAtom" - b "kx.LongAtom" "kx.LongAtom" + columns datatypes + ---------------------- + a "kx.LongAtom" + a_float "kx.FloatAtom" + b "kx.LongAtom" ')) ``` """ @@ -10565,8 +10617,8 @@ def len(self, iterator=None): ... 'a': [1, -1, 0], ... 'b': [[-1, 2, 1], [0, 2], 1] ... }) - >>> tab.exec(kx.Column('b').len(iterator='each'))) - pykx.LongVector(pykx.q('3 3 3')) + >>> tab.exec(kx.Column('b').len(iterator='each')) + pykx.LongVector(pykx.q('3 2 1')) ``` """ return self.call('count', iterator=iterator) @@ -11369,10 +11421,8 @@ def key(self, iterator=None): >>> d1 = kx.q('`a`b`c ! 1 2 3') >>> d2 = kx.q('`d`e`f ! 4 5 6') >>> t = kx.Table(data={'n':[d1, d2]}) - >>> t.exec(kx.Column('n').type(iterator='each')) - pykx.Table(pykx.q(' - n - ----- + >>> t.exec(kx.Column('n').key(iterator='each')) + pykx.List(pykx.q(' a b c d e f ')) @@ -11487,7 +11537,7 @@ def next(self, iterator=None): ```python >>> import pykx as kx >>> t = kx.q('([] n:3 6 9 12)') - >>> t.exec(kx.Column('n').next())) + >>> t.exec(kx.Column('n').next()) pykx.LongVector(pykx.q('6 9 12 0N')) ``` """ diff --git a/tests/data/types.parquet b/tests/data/types.parquet new file mode 100644 index 0000000..2df098f Binary files /dev/null and b/tests/data/types.parquet differ diff --git a/tests/test_cloud_edition.py b/tests/test_cloud_edition.py index a98b14e..1d78bd9 100644 --- a/tests/test_cloud_edition.py +++ b/tests/test_cloud_edition.py @@ -6,7 +6,8 @@ import pytest -if platform.system() != 'Linux' or platform.machine() == 'aarch64': +if platform.system() != 'Linux' or platform.machine() == 'aarch64'\ + or os.getenv('SKIP_LIC_TESTS') is not None: pytest.skip( 'KDB-X Cloud Edition functionality only supports' ' x86_64 Linux', diff --git a/tests/test_ipc.py b/tests/test_ipc.py index 5f8157d..214e7a2 100644 --- a/tests/test_ipc.py +++ b/tests/test_ipc.py @@ -87,6 +87,14 @@ def test_ipc_messaging_tcp(kx, q_port): _generic_ipc_test(q) +@pytest.mark.unlicensed +@pytest.mark.parametrize('cls_name', ['QConnection', 'SyncQConnection']) +def test_single_handle_no_double_init(kx, q_port, cls_name): + cls = getattr(kx, cls_name) + with cls('localhost', q_port) as q: + assert int(q('count .z.W')) == 1 + + @pytest.mark.unlicensed @pytest.mark.parametrize('q_init', [[b'.z.pw:{(x~`username)&y~"password"}\n']]) def test_ipc_messaging_tcp_auth(kx, q_port): @@ -330,7 +338,7 @@ def test_py_file_execution(kx): proc = subprocess.Popen( [q_exe_path, '-p', '15005'], stdout=subprocess.DEVNULL, - stderr=subprocess.STDOUT + stderr=subprocess.STDOUT, ) time.sleep(2) with kx.SyncQConnection(port=15005) as q: @@ -719,6 +727,39 @@ def test_sync_helpful_error_for_closed_conn(kx, q_port): q('hclose .z.w; til 10') +@pytest.mark.unlicensed +def test_sync_close_finalizes_when_shutdown_fails(kx, q_port): + conn = kx.SyncQConnection(port=q_port) + real_sock = conn._sock + state = {'sock_closed': False, 'finalized': False} + + class _ShutdownFails: + def shutdown(self, *args, **kwargs): + raise OSError('simulated dead peer') + + def close(self, *args, **kwargs): + state['sock_closed'] = True + return real_sock.close(*args, **kwargs) + + def __getattr__(self, name): + return getattr(real_sock, name) + + real_finalizer = conn._finalizer + + def _tracked_finalizer(): + state['finalized'] = True + return real_finalizer() + + object.__setattr__(conn, '_sock', _ShutdownFails()) + object.__setattr__(conn, '_finalizer', _tracked_finalizer) + + conn.close() + + assert conn.closed + assert state['sock_closed'], 'socket was not closed after shutdown failure' + assert state['finalized'], 'finalizer did not run after shutdown failure' + + def check_enough_memory(GiB): import psutil minimum_memory = GiB diff --git a/tests/test_license.py b/tests/test_license.py index 86a9060..dcfdcee 100644 --- a/tests/test_license.py +++ b/tests/test_license.py @@ -2,6 +2,7 @@ from io import StringIO import os import re +import sys # Do not import pykx here - use the `kx` fixture instead! import pytest @@ -498,15 +499,20 @@ def test_expired_license(monkeypatch): os.environ['QLIC'] = os.environ['QHOME'] = lic_folder inputs = iter(['n', 'n']) monkeypatch.setattr('builtins.input', lambda _: next(inputs)) + monkeypatch.setattr(sys, 'ps1', '>>> ', raising=False) import pykx as kx assert not kx.licensed + with pytest.raises(Exception) as e: + kx.q('1+1') + assert "license is expired" in str(e) @pytest.mark.skipif( os.getenv('SKIP_LIC_TESTS') is not None, reason='License tests are being skipped' ) +@pytest.mark.xfail(reason='Flaky test', strict=False) def test_mode_licensed_true(): os.environ['PYKX_LICENSED'] = 'True' lic_folder = '/tmp/license' @@ -520,6 +526,7 @@ def test_mode_licensed_true(): kx.licensed +@pytest.mark.xfail(reason='Flaky license initialization ordering', strict=False) @pytest.mark.skipif( os.getenv('SKIP_LIC_TESTS') is not None, reason='License tests are being skipped' @@ -539,6 +546,7 @@ def test_mode_monkeypatch_licensed_true(monkeypatch): kx.licensed +@pytest.mark.xfail(reason='Flaky license initialization ordering', strict=False) @pytest.mark.skipif( os.getenv('PYKX_THREADING') is not None, reason='KXI-63218 PYKX_THREADING licence path differs' @@ -556,10 +564,16 @@ def test_mode_monkeypatch_licensed_false(monkeypatch): os.environ['QLIC'] = os.environ['QHOME'] = lic_folder inputs = iter(['n', 'n']) + monkeypatch.setattr('builtins.input', lambda _: next(inputs)) + monkeypatch.setattr(sys, 'ps1', '>>> ', raising=False) import pykx as kx assert not kx.licensed + with pytest.raises(Exception) as err: + kx.q('1+1') + assert "corrupt" in str(err.value) + print(str(err.value)) @pytest.mark.skipif( @@ -570,6 +584,7 @@ def test_mode_monkeypatch_licensed_false(monkeypatch): os.getenv('SKIP_LIC_TESTS') is not None, reason='License tests are being skipped' ) +@pytest.mark.xfail(reason='Flaky test', strict=False) def test_bad_license(): lic_folder = '/tmp/license' os.makedirs(lic_folder, exist_ok=True) @@ -579,3 +594,76 @@ def test_bad_license(): import pykx as kx assert not kx.licensed + + +@pytest.mark.skipif( + os.getenv('PYKX_THREADING') is not None, + reason='KXI-63218 PYKX_THREADING licence path differs' +) +@pytest.mark.skipif( + os.getenv('SKIP_LIC_TESTS') is not None, + reason='License tests are being skipped' +) +def test_unlicensed_reason_env_var(): + os.environ['PYKX_UNLICENSED'] = 'True' + import pykx as kx + assert not kx.licensed + with pytest.raises(Exception) as err: + kx.q('1+1') + assert "PYKX_UNLICENSED" in str(err.value) + + +@pytest.mark.skipif( + os.getenv('PYKX_THREADING') is not None, + reason='KXI-63218 PYKX_THREADING licence path differs' +) +@pytest.mark.skipif( + os.getenv('SKIP_LIC_TESTS') is not None, + reason='License tests are being skipped' +) +def test_unlicensed_reason_qargs(): + os.environ['QARGS'] = '--unlicensed' + import pykx as kx + assert not kx.licensed + with pytest.raises(Exception) as err: + kx.q('1+1') + assert "--unlicensed" in str(err.value) + + +@pytest.mark.skipif( + os.getenv('PYKX_THREADING') is not None, + reason='KXI-63218 PYKX_THREADING licence path differs' +) +@pytest.mark.skipif( + os.getenv('SKIP_LIC_TESTS') is not None, + reason='License tests are being skipped' +) +def test_unlicensed_reason_light_load(): + os.environ['PYKX_LIGHT_LOAD'] = 'true' + import pykx as kx + assert not kx.licensed + with pytest.raises(Exception) as err: + kx.q('1+1') + assert "light_load" in str(err.value) + + +@pytest.mark.skipif( + os.getenv('PYKX_THREADING') is not None, + reason='KXI-63218 PYKX_THREADING licence path differs' +) +@pytest.mark.skipif( + os.getenv('SKIP_LIC_TESTS') is not None, + reason='License tests are being skipped' +) +def test_unlicensed_reason_no_license_found(tmp_path, monkeypatch): + os.environ['QLIC'] = os.environ['QHOME'] = str(tmp_path.absolute()) + + inputs = iter(['N', 'N']) + monkeypatch.setattr('builtins.input', lambda _: next(inputs)) + monkeypatch.setattr(sys, 'ps1', '>>> ', raising=False) + + import pykx as kx + assert not kx.licensed + with pytest.raises(Exception) as e: + kx.q('1+1') + assert "No license was found" in str(e) diff --git a/tests/test_pandas_api.py b/tests/test_pandas_api.py index 30c9250..4a805b6 100644 --- a/tests/test_pandas_api.py +++ b/tests/test_pandas_api.py @@ -1464,6 +1464,11 @@ def test_df_rename(kx, q): rez = t.rename({'Poliddcy': 'PolicyID', 'Policy': 'p_id'}, axis=1) assert all(t.pd().rename({'Poliddcy': 'PolicyID', 'Policy': 'p_id'}, axis=1).eq(rez.pd())) + t = kx.q('([] Policy: 1 2 3)') + cols = {0: 'ignored', 'Policy': 'PolicyID'} + rez = t.rename(cols, axis=1) + assert all(t.pd().rename(cols, axis=1).eq(rez.pd())) + mixed_index_table = kx.q('([] id:(`a;2;1); Policy: 3 4 5; name:`a`b`c)').pd() df=kx.toq(mixed_index_table.set_index(['id'])) rez = df.rename({1: 'x', 2: 'j'}) diff --git a/tests/test_reset_index.py b/tests/test_pandas_reset_index.py similarity index 87% rename from tests/test_reset_index.py rename to tests/test_pandas_reset_index.py index 4d166d1..65372a9 100644 --- a/tests/test_reset_index.py +++ b/tests/test_pandas_reset_index.py @@ -37,6 +37,14 @@ def test_reset_drop(q): assert q('~', df.reset_index(['x', 'y'], drop=True), pddf.reset_index(['x', 'y'], drop=True)) +def test_reset_int_drop(q): + df = q('([x: til 10; y: 10 - til 10]z: 10?`a`b`c)') + pddf = df.pd() + assert q('~', df.reset_index(0, drop=True), pddf.reset_index(0, drop=True)) + assert q('~', df.reset_index(1, drop=True), pddf.reset_index(1, drop=True)) + assert q('~', df.reset_index([0, 1], drop=True), pddf.reset_index([0, 1], drop=True)) + + def test_reset_duplicates(kx, q): df = q('([til 10;10?1f];10?1f;10?1f)') assert q('~', df.reset_index(allow_duplicates=True), q('0!', df)) diff --git a/tests/test_pykx.py b/tests/test_pykx.py index 5b1e542..0c6580e 100644 --- a/tests/test_pykx.py +++ b/tests/test_pykx.py @@ -439,26 +439,34 @@ def test_kx_versions(kx): test_K = str(kx.q.z.K.py()) test_vars = (test_os, test_arch, test_K) if test_vars == ('Linux', 'x86_64', '5.0'): - assert kx.q.z.k == kx.q('2026.05.01') + assert kx.q.z.k == kx.q('2026.07.23') elif test_vars == ('Linux', 'aarch64', '5.0'): - assert kx.q.z.k == kx.q('2026.05.01') + assert kx.q.z.k == kx.q('2026.07.23') elif test_vars == ('Darwin', 'x86_64', '5.0'): - assert kx.q.z.k == kx.q('2026.05.01') + assert kx.q.z.k == kx.q('2026.07.23') elif test_vars == ('Darwin', 'arm64', '5.0'): - assert kx.q.z.k == kx.q('2026.05.01') + assert kx.q.z.k == kx.q('2026.07.23') elif test_vars == ('Windows', 'AMD64', '5.0'): - assert kx.q.z.k == kx.q('2026.05.01') + assert kx.q.z.k == kx.q('2026.07.23') else: raise AssertionError(f"Unexpected env: {test_vars}") @pytest.mark.isolate +@pytest.mark.skipif( + os.getenv('SKIP_LIC_TESTS') is not None, + reason='License tests are being skipped' +) def test_default_qce(): import pykx as kx assert not kx.q('@[{2 `datetime$`date$x}', dt).py() + + +@pytest.fixture +def tmp_date_csv_path(tmp_path): + p = tmp_path/'date.csv' + p.write_text( + 'd,n\n' + '2023.01.01,0\n' + '2023.06.15,1\n' + '2023.12.31,2\n' + ) + yield p + p.unlink() + + +@pytest.mark.ipc +def test_read_csv_date_type(kx, q, tmp_date_csv_path): + tab = q.read.csv(tmp_date_csv_path, [kx.DateAtom, kx.LongAtom]) + assert isinstance(tab, kx.Table) + + d = q('{x`d}', tab) + assert isinstance(d, kx.DateVector) + assert q('{type x}', d).py() == 14 + assert q('{all x = 2023.01.01 2023.06.15 2023.12.31}', d).py() + + @pytest.fixture def tmp_fixed_path(tmp_path): p = tmp_path/'tmp.fixed' diff --git a/tests/test_reimport.py b/tests/test_reimport.py index 08e1d46..a7641e1 100644 --- a/tests/test_reimport.py +++ b/tests/test_reimport.py @@ -1,15 +1,82 @@ # Do not import pykx here - use the `kx` fixture instead! +import os import subprocess +import sys +# --- Reimport no longer requires the PyKXReimport helper (Python child) ----- + def test_reimport(kx): - with kx.PyKXReimport(): - ret = subprocess.run(["python", "-c", "import pykx"]) + ret = subprocess.run([sys.executable, "-c", "import pykx"]) assert 0 == ret.returncode def test_reimport_kdefault(kx): + ret = subprocess.run( + [sys.executable, "-c", "import os;os.environ['PYKX_DEFAULT_CONVERSION']='k';import pykx"]) + assert 0 == ret.returncode + + +# --- PyKXReimport remains available and functional -------------------------- +# (Still required e.g. for spawning kdb+ 4.0/4.1 q subprocesses from Python - +# see tests/manual_reimport_matrix.py for the full cross-process matrix.) + +def test_reimport_old(kx): + with kx.PyKXReimport(): + ret = subprocess.run([sys.executable, "-c", "import pykx"]) + assert 0 == ret.returncode + + +def test_reimport_kdefault_old(kx): with kx.PyKXReimport(): ret = subprocess.run( - ["python", "-c", "import os;os.environ['PYKX_DEFAULT_CONVERSION']='k';import pykx"]) + [sys.executable, "-c", + "import os;os.environ['PYKX_DEFAULT_CONVERSION']='k';import pykx"]) assert 0 == ret.returncode + + +def test_pykxreimport_qhome_reset_restore(kx): + # Inside the context QHOME is reset to the user's original QHOME; on exit the + # pykx-internal QHOME is restored. + qhome_before = os.environ.get('QHOME') + with kx.PyKXReimport(): + assert os.environ['QHOME'] == os.environ['PYKX_OLD_QHOME'] + assert os.environ.get('QHOME') == qhome_before + + +# --- Environment-variable regressions --------------------------------------- + +def test_pykx_old_qpath_populated(kx): + # Regression: PYKX_OLD_QHOME/QPATH must hold real values so a child process + # can restore them. PYKX_OLD_QPATH used to always be empty. + assert os.environ.get('PYKX_OLD_QHOME', '') != '' + assert os.environ.get('PYKX_OLD_QPATH', '') != '' + + +def test_qpath_no_duplicate_on_nested_import(kx, tmp_path): + # Regression: nested KDB-X Python imports must not keep appending + # `$QHOME/mod` to QPATH (it used to grow one entry per nesting level). + script = tmp_path / "nested_import.py" + script.write_text( + "import os, sys, subprocess\n" + "import pykx # noqa: F401\n" + "qp = os.environ.get('QPATH', '')\n" + "parts = [p for p in qp.split(os.pathsep) if p]\n" + "assert len(parts) == len(set(parts)), 'duplicate QPATH entries: ' + qp\n" + "depth = int(sys.argv[1]) if len(sys.argv) > 1 else 0\n" + "if depth < 3:\n" + " sys.exit(subprocess.run([sys.executable, sys.argv[0], str(depth + 1)]).returncode)\n" + ) + ret = subprocess.run([sys.executable, str(script), "0"]) + assert 0 == ret.returncode + + +def test_setdefault_does_not_export_env(kx): + # Regression: `.pykx.setdefault` must only change the current process, it must + # NOT export PYKX_DEFAULT_CONVERSION (which would leak to child processes). + before = os.environ.get('PYKX_DEFAULT_CONVERSION') + try: + kx.q('.pykx.setdefault["pd"]') + assert os.environ.get('PYKX_DEFAULT_CONVERSION') == before + finally: + kx.q('.pykx.setdefault["default"]') diff --git a/tests/test_schema.py b/tests/test_schema.py index eb13958..df4839a 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -8,10 +8,11 @@ def test_simple_schema(q, kx): 'col1': kx.GUIDAtom, 'col2': kx.TimeAtom, 'col3': kx.BooleanAtom, - 'col4': kx.FloatVector}) + 'col4': kx.FloatVector, + 'col5': kx.SecondVector}) assert isinstance(qtab, kx.Table) - assert kx.q.cols(qtab).py() == ['col1', 'col2', 'col3', 'col4'] - assert kx.q('{exec t from 0!meta x}', qtab).py() == b'gtbf' + assert kx.q.cols(qtab).py() == ['col1', 'col2', 'col3', 'col4', 'col5'] + assert kx.q('{exec t from 0!meta x}', qtab).py() == b'gtbfv' def test_single_key_schema(q, kx): diff --git a/tests/test_sql.py b/tests/test_sql.py index 5dda2a4..6036bc1 100644 --- a/tests/test_sql.py +++ b/tests/test_sql.py @@ -170,7 +170,8 @@ def test_sql_get_input_values(q, kx): ) @pytest.mark.embedded def test_sql_string_col(q): - q('t:([] optid:1 2 3;Market:`a`b`CBOE;date:3#2023.11.14;Symbol:("a";"b";"odMP=20"))') + q('''t:([] optid:1 2 3;Market:`a`b`CBOE;date:3#2023.11.14; + Symbol:(enlist "a";enlist "b";"odMP=20"))''') qres = q('''select optid,Market,Symbol from t where date = 2023.11.14,Market=`CBOE,Symbol like "odMP=20"''') sqlres = q("""s)select optid,Market,Symbol from t diff --git a/tests/test_toq.py b/tests/test_toq.py index 3a229e3..1cd39f4 100644 --- a/tests/test_toq.py +++ b/tests/test_toq.py @@ -5,6 +5,7 @@ from pathlib import Path from platform import system from sys import getrefcount +from threading import Thread from uuid import UUID, uuid4 # Do not import Pandas, PyArrow, or KDB-X Python here - use the pd/pa/kx fixtures instead! @@ -1008,6 +1009,68 @@ def test_from_numpy_ndarray_3(kx): assert all(minute_array.np() == minute_array_np) +@pytest.mark.nep49 +def test_from_numpy_recarray(kx): + # A structured/record array (as produced by ``Table.np``) converts to a q Table with one + # column per named field, rather than raising a TypeError (KXI-71774). This holds both for a + # ``numpy.recarray`` and for a plain structured ``numpy.ndarray``. + rec = np.array([(1, b'a'), (2, b'b')], dtype=[('x', 'i8'), ('y', 'S1')]) + for arr in (rec, rec.view(np.recarray)): + tab = kx.toq(arr) + assert isinstance(tab, kx.Table) + assert kx.K(arr).py() == tab.py() + # The 'S1' field 'y' becomes a q char column (one char per row). + assert tab.py() == {'x': [1, 2], 'y': b'ab'} + + # The reported round-trip must work out of the box: a single row char column. + single_char = kx.q('([] c:enlist " ")') + assert kx.toq(single_char.np()).py() == single_char.py() + + # A range of column types should round-trip through ``Table.np`` -> ``toq`` for both the + # recarray and the plain structured ndarray forms. + tab = kx.q('([] a:1 2 3; b:`x`y`z; c:"abc"; d:1.1 2.2 3.3; e:0x010203; f:3?0Ng)') + assert kx.q('~', kx.toq(tab.np()), tab).py() + assert kx.q('~', kx.toq(np.asarray(tab.np())), tab).py() + + # Empty tables preserve their (char) column typing rather than degrading to a list. + empty = kx.q('([] c:0#" ")') + assert kx.q('~', kx.toq(empty.np()), empty).py() + + # 'U1' (str) fields become symbol columns, 'S1' (bytes) fields become char columns. + mixed = np.array([('a', b'z')], dtype=[('s', 'U1'), ('b', 'S1')]) + mtab = kx.toq(mixed) + assert kx.q('type', mtab['s']).py() == 11 # symbol vector + assert kx.q('type', mtab['b']).py() == 10 # char vector + + # ktype=KeyedTable is supported (keyed on a generated index). + assert isinstance(kx.toq(tab.np(), ktype=kx.KeyedTable), kx.KeyedTable) + assert isinstance(kx.toq(tab.np(), ktype=kx.Table), kx.Table) + + # strings_as_char converts symbol fields to lists of char vectors. + sac = kx.toq(tab.np(), strings_as_char=True) + assert kx.q('type', sac['b']).py() == 0 # general list of char vectors + + # handle_nulls preserves q nulls through the round-trip. + with_nulls = kx.q('([] a:1 2 0N 4; d:1.1 2.2 0n 4.4)') + assert kx.q('~', kx.toq(with_nulls.np(), handle_nulls=True), with_nulls).py() + + # Unsupported ktypes and multi-dimensional structured arrays raise TypeError. + with pytest.raises(TypeError): + kx.toq(rec, kx.LongVector) + with pytest.raises(TypeError): + kx.toq(np.zeros((2, 2), dtype=[('a', 'i8')])) + + +@pytest.mark.nep49 +def test_from_numpy_single_char(kx): + # The default conversion of a length-one 'S1' (bytes) array remains a CharAtom, matching the + # existing behaviour where a single byte becomes an atom (KXI-71774). + assert isinstance(kx.toq(np.array([b' '], dtype='S1')), kx.CharAtom) + # A CharVector can still be forced explicitly from a single byte. + assert isinstance(kx.CharVector(b' '), kx.CharVector) + assert kx.CharVector(b' ').py() == b' ' + + @pytest.mark.unlicensed @pytest.mark.nep49 def test_from_numpy_incompatible_types(kx): @@ -1373,6 +1436,49 @@ def test_from_arrow(kx, pa, pd): assert a.combine_chunks() == kx.K(a).pa() +@pytest.mark.licensed +@pytest.mark.nep49 +def test_from_arrow_empty_string_column(kx, pd, pa): + schema = pa.schema([('s', pa.large_string()), ('i', pa.int64())]) + via_string = isinstance(schema.empty_table().to_pandas()['s'].dtype, pd.StringDtype) + empty_str_type = 11 if via_string else 0 + + empty = kx.toq(schema.empty_table()) + assert kx.q('type', empty['s']).py() == empty_str_type + assert kx.q('type', empty['i']).py() == 7 + + all_null = kx.toq(pa.table({'s': pa.array([None, None], type=pa.large_string())})) + assert kx.q('type', all_null['s']).py() == empty_str_type + if via_string: + assert kx.q('~', all_null['s'], kx.q('2#`')).py() + + populated = kx.toq(pa.table({'s': pa.array(['a', None], type=pa.large_string())})) + assert kx.q('type', populated['s']).py() == 11 + assert kx.q('~', populated['s'], kx.q('`a`')).py() + + +@pytest.mark.licensed +@pytest.mark.nep49 +def test_from_pandas_empty_string_column(kx, pd): + for data in ([], [None], [None, None]): + df = pd.DataFrame({'s': pd.Series(data, dtype='string')}) + assert kx.q('type', kx.toq(df)['s']).py() == 11 + assert kx.q('type', kx.toq(df, strings_as_char=True)['s']).py() == 0 + + +@pytest.mark.licensed +@pytest.mark.nep49 +def test_from_pandas_arrow_string_column(kx, pd, pa): + if not hasattr(pd, 'ArrowDtype'): + pytest.skip('pd.ArrowDtype requires pandas>=1.5') + dtype = pd.ArrowDtype(pa.large_string()) + for data in ([], [pd.NA], [pd.NA, pd.NA], ['a', pd.NA], ['a', 'b']): + col = kx.toq(pd.DataFrame({'s': pd.Series(data, dtype=dtype)}))['s'] + assert kx.q('type', col).py() == 11 + mixed = kx.toq(pd.DataFrame({'s': pd.Series(['a', pd.NA], dtype=dtype)}))['s'] + assert kx.q('~', mixed, kx.q('`a`')).py() + + @pytest.mark.licensed @pytest.mark.nep49 def test_null_roundtrip(kx): @@ -1760,3 +1866,61 @@ def test_2d_array_from_file_no_allocator(kx): def test_embedding_segfault(kx): df=pd.DataFrame(dict(embeddings=list(np.random.ranf((500, 10)).astype(np.float32)))) kx.toq(df) + + +def test_table_in_thread(kx): + + def generate_data(): + now = datetime.utcnow() + trade_id = uuid4() + df = pd.DataFrame( + [[now, "VOD", "LSE", "buy", 75.90, 100, trade_id]], + columns=["time", "sym", "exch", "side", "price", "size", "tradeID"] + ) + return kx.Table(df) + + def generate_data_in_thread(): + res = [] + + def thread_body(): + d = generate_data() + res.append(d) + t = Thread(target=thread_body) + t.start() + t.join() + return res[0] + + assert isinstance(generate_data_in_thread(), kx.Table) + + +@pytest.mark.licensed_only +def test_keep_local_times_off(kx): + a = pd.Timestamp('2026-07-28 15:41:22.734077-0400', tz='America/New_York') + b = pd.Series([a, a]) + + k = kx.q('2026.07.28D15:41:22.734077000') + kk = kx.q('2026.07.28D15:41:22.734077000 2026.07.28D15:41:22.734077000') + k2 = kx.q('2026.07.28D19:41:22.734077000') + kk2 = kx.q('2026.07.28D19:41:22.734077000 2026.07.28D19:41:22.734077000') + + assert kx.toq(a) == k2 + assert (kx.toq(b) == kk2).all() + assert kx.toq(a) != k + assert not (kx.toq(b) == kk).any() + + +@pytest.mark.isolate +def test_keep_local_times_on(): + import os + os.environ['PYKX_KEEP_LOCAL_TIMES'] = '1' + import pykx as kx + import pandas as pd + + a = pd.Timestamp('2026-07-28 15:41:22.734077-0400', tz='America/New_York') + b = pd.Series([a, a]) + + k = kx.q('2026.07.28D15:41:22.734077000') + kk = kx.q('2026.07.28D15:41:22.734077000 2026.07.28D15:41:22.734077000') + + assert kx.toq(a) == k + assert (kx.toq(b) == kk).all() diff --git a/tests/test_util.py b/tests/test_util.py index a76df66..76de4f6 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -106,22 +106,41 @@ def time_when_first_run(): @pytest.mark.ipc -def test_pickle_pykx_df_block_manager(kx, q): +def test_pickle_pykx_df_block_manager(q): """Test that a Pandas DataFrame created by KDB-X Python can be deserialized without KDB-X Python. - DataFrames that originate from KDB-X Python have a custom block manager. We have to take care to - serialize it as a regular block manager so that it can be deserialized without - KDB-X Python installed. + On Pandas <3.0, DataFrames that originate from KDB-X Python use a custom block manager + (`pykx.util.BlockManagerUnconsolidated`). We have to take care to serialize it as a regular + block manager so that it can be deserialized without KDB-X Python installed. On Pandas 3.0+ + `.pd()` already builds a standard `BlockManager`, so nothing KDB-X specific is present. """ df = q('([] date:9?.z.D; id:9?9; time:9?.z.N; bs:9?9; bp:9?9f; ap:9?9f; as:9?9)').pd() serialized = pickle.dumps(df) assert b'pykx' not in serialized - if not kx.config.pandas_gt2: - # `df._data` is the block manager - assert type(df._data).__name__.encode() not in serialized + assert b'BlockManagerUnconsolidated' not in serialized + + +def test_pykx_df_equals_both_directions(kx, q): + """`DataFrame.equals` comparing a KDB-X Python DataFrame with a native Pandas DataFrame. + + Pandas 2.2.0 started comparing the `_mgr` type in `DataFrame.equals`. That makes the reverse + direction (KDB-X Python DataFrame as the caller) return False only while `.pd()` still uses + its custom `_mgr` - i.e. Pandas 2.2 up to (but not including) 3.0 (a documented known issue, + see docs/help/issues.md). Before 2.2 there is no `_mgr` check, and on 3.0+ `.pd()` builds a + standard `BlockManager`, so both directions are symmetric. + """ + import pandas as pd + df1 = pd.DataFrame({'cl': ['foo']}) + df2 = q('([] cl:enlist `foo)').pd() + # Native Pandas DataFrame as caller is True on every supported Pandas version. + assert df1.equals(df2) + pandas_version = tuple(int(x) for x in pd.__version__.split('.')[:2]) + reverse_asymmetric = pandas_version >= (2, 2) and not kx.config.pandas_gt2 + if reverse_asymmetric: + # Known limitation on Pandas 2.2 to 2.x: custom `_mgr` makes this direction False. + assert not df2.equals(df1) else: - # `df._mgr` is the block manager in Pandas 3 - assert type(df._mgr).__name__.encode() not in serialized + assert df2.equals(df1) @pytest.mark.unlicensed diff --git a/tests/test_virtual_table.py b/tests/test_virtual_table.py new file mode 100644 index 0000000..2151190 --- /dev/null +++ b/tests/test_virtual_table.py @@ -0,0 +1,201 @@ +from pathlib import Path + +import pytest + + +class TestVirtualTable: + + def make_tab(self, kx): + pq = kx.module.use('kx.pq') + pq.t = kx.module.use('kx.pq.t') + tab = pq.pq(Path('tests/data/types.parquet')) + return tab + + def test_select(self, kx): + tab = self.make_tab(kx) + assert kx.q('~', tab.select(kx.Column('col0') & kx.Column('col1')), + kx.q('flip `col0`col1!(110100100b;1 2 3 4 5 6 7 8 9)')).py() + + def test_exec(self, kx): + tab = self.make_tab(kx) + assert kx.q('~', tab.exec(kx.Column('col1')), kx.q('1 2 3 4 5 6 7 8 9')).py() + + errStr = "pykx.VirtualTable objects cannot be operated on directly, you must .select() or .exec() data from them first" # noqa: E501 + + # Conversion methods + def test_cast(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).cast(kx.FloatAtom) + assert self.errStr in str(err.value) + + def test_py(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).py() + assert self.errStr in str(err.value) + + def test_np(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).np() + assert self.errStr in str(err.value) + + def test_pd(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).pd() + assert self.errStr in str(err.value) + + def test_pa(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).pa() + assert self.errStr in str(err.value) + + # Utility methods + def test_any(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).any() + assert self.errStr in str(err.value) + + def test_all(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).all() + assert self.errStr in str(err.value) + + def test_copy(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).copy() + assert self.errStr in str(err.value) + + def test_is_atom(self, kx): + assert self.make_tab(kx).is_atom is False + + # Comparison operators + def test_lt(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) < self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_le(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) <= self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_eq(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) == self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_ne(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) != self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_gt(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) > self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_ge(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) >= self.make_tab(kx) + assert self.errStr in str(err.value) + + # Arithmetic operators + def test_add(self, kx): + with pytest.raises(kx.QError) as err: + assert self.make_tab(kx) + self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_radd(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__radd__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_sub(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx) - self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_rsub(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rsub__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_mul(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx) * self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_rmul(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rmul__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_truediv(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx) / self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_rtruediv(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rtruediv__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_floordiv(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx) // self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_rfloordiv(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rfloordiv__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_mod(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx) % self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_rmod(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rmod__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_divmod(self, kx): + with pytest.raises(kx.QError) as err: + divmod(self.make_tab(kx), self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_rdivmod(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rdivmod__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_pow(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx) ** self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_rpow(self, kx): + with pytest.raises(kx.QError) as err: + self.make_tab(kx).__rpow__(self.make_tab(kx)) + assert self.errStr in str(err.value) + + # Unary operators + def test_neg(self, kx): + with pytest.raises(kx.QError) as err: + -self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_pos(self, kx): + with pytest.raises(kx.QError) as err: + +self.make_tab(kx) + assert self.errStr in str(err.value) + + def test_abs(self, kx): + with pytest.raises(kx.QError) as err: + abs(self.make_tab(kx)) + assert self.errStr in str(err.value) + + def test_bool(self, kx): + with pytest.raises(kx.QError) as err: + bool(self.make_tab(kx)) + assert self.errStr in str(err.value) diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index 16d0c67..4968833 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -15,6 +15,7 @@ from uuid import UUID import itertools import sys +import warnings # Do not import Pandas, PyArrow, or pykx here - use the pd/pa/kx fixtures instead! import numpy as np @@ -724,6 +725,15 @@ def test_is_null_and_is_inf(self, q): assert not q('{x*y+z}').is_null assert not q('{x*y+z}').is_inf + for expr in ('0Ng', '0Nh', '0Ne', '0Nf', '0Np', '0Nn', '0Nu', '0Nv', '0Nt', '" "', '`'): + atom = q(expr) + for prop in ('is_null', 'is_inf', 'is_pos_inf', 'is_neg_inf'): + assert type(getattr(atom, prop)) is bool, f'{expr}.{prop}' + for expr in ('0We', '-0We', '0Wf', '-0Wf', '0Wh', '-0Wh'): + atom = q(expr) + for prop in ('is_inf', 'is_pos_inf', 'is_neg_inf'): + assert type(getattr(atom, prop)) is bool, f'{expr}.{prop}' + @pytest.mark.nep49 def test_null_np(self, q, kx): for type_char in 'hij': @@ -1084,6 +1094,25 @@ def test_arr(self, kx, q): assert np.asarray(kx.DateAtom(datetime(2003, 4, 5))).dtype == "= version.parse('2.0') + for obj in (kx.LongAtom(42), kx.q('til 5')): + assert np.asarray(obj, dtype=object).dtype == object + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + assert np.array(obj) is not None + assert np.array(obj, copy=True) is not None + if numpy_gt2: + # A dtype conversion is unavoidably a copy, so an explicit copy=False must + # be refused. NumPy 1.x has lenient copy semantics and does not raise here. + with pytest.raises(ValueError): + np.array(obj, dtype=object, copy=False) + @pytest.mark.unlicensed def test_arr_nulls(self, kx, q): assert np.asarray(kx.ShortAtom(None)).dtype == "int16" @@ -1817,6 +1846,10 @@ def f(type_code, zero): assert q(f'@[v;-3?50;:;0N{type_code}]').has_nulls assert q(f'@[v;-3?50;:;0W{type_code}]').has_infs + for expr in (v, q(f'@[v;-3?50;:;0N{type_code}]'), q(f'@[v;-3?50;:;0W{type_code}]')): + assert type(expr.has_nulls) is bool, f'{type_code}.has_nulls' + assert type(expr.has_infs) is bool, f'{type_code}.has_infs' + types = ( ('h', '0h'), ('i', '0i'), ('j', '0j'), ('e', '10000e'), ('f', '10000f'), ('p', '0p'), ('m', '2000.01m'), ('d', '2000.01.01d'), ('n', '00:00:00.000000000'), ('u', '00:00'), @@ -1825,6 +1858,21 @@ def f(type_code, zero): for type_code, zero in types: f(type_code, zero) + for expr in ('0Ng,3?0Ng', 'v where not null v:10?0Ng', '011011100b', '0xdeadbeef'): + vec = q(expr) + assert type(vec.has_nulls) is bool, f'{expr}.has_nulls' + assert type(vec.has_infs) is bool, f'{expr}.has_infs' + + def test_has_infs_negative_infinity(self, q, kx): + assert isinstance(q('-0w -0w -0w'), kx.FloatVector) + assert q('-0w -0w -0w').has_infs + assert isinstance(q('-0we, -0we, -0we'), kx.RealVector) + assert q('-0we, -0we, -0we').has_infs + assert q('1 2 -0w').has_infs + assert q('1 2 0w').has_infs + assert not q('1 2 3f').has_infs + assert type(q('-0w -0w').has_infs) is bool + def test_np_timestampvector_nulls(self, kx): assert pd.isna(kx.q('0Np').py()) assert kx.q('enlist 0Np').py() == [kx.TimestampAtom(kx.q('0Np'))] @@ -2430,6 +2478,27 @@ def test_empty_vector(self, q): assert q('"p"$()').np().dtype == np.dtype('datetime64[ns]') assert q('"p"$()').np(raw=True).dtype == np.int64 + @pytest.mark.nep49 + def test_py_multiple_nulls(self, q, kx): + vec = kx.q('(2023.01.01D12:00:00.000000000; 0Np; 2023.01.03D06:30:00.000000000; 0Np; 0Np)') + assert isinstance(vec, kx.TimestampVector) + result = vec.py() + assert result[0] == datetime(2023, 1, 1, 12, 0, 0) + assert result[2] == datetime(2023, 1, 3, 6, 30, 0) + assert result[1] is pd.NaT + assert result[3] is pd.NaT + assert result[4] is pd.NaT + assert None not in result + + @pytest.mark.nep49 + def test_py_tzinfo_nulls(self, kx): + vec = kx.q('(2023.01.01D12:00:00.000000000; 0Np; 2023.01.03D06:30:00.000000000)') + result = vec.py(tzinfo=pytz.timezone('GMT')) + assert result[0] == datetime(2023, 1, 1, 12, 0, 0, tzinfo=pytz.utc) + assert result[0].tzinfo is not None + assert result[2] == datetime(2023, 1, 3, 6, 30, 0, tzinfo=pytz.utc) + assert result[1] is pd.NaT + class Test_MonthVector: q_vec_str = '2006.04 1947.10 1876.04 2170.01m' @@ -2901,10 +2970,20 @@ def test_pa(self, q, pa): t = q(self.q_table_str) assert all(t.pa().to_pandas() == t.pd(raw_guids=True)) + def test_pd_raw_guids_repr(self, q): + # A raw_guids=True DataFrame must be renderable: pandas/numpy call + # PandasUUIDArray.__array__(dtype) when formatting, which previously + # raised `TypeError: __array__() takes 1 positional argument but 2 were given`. + df = q(self.q_table_str).pd(raw_guids=True) + assert repr(df) + assert np.asarray(df['c'].values, dtype=object).shape == (3,) + def test_has_null_and_has_inf(self, q): table = q('([]0w,9?1f;0n,9?1f)') assert table.has_nulls assert table.has_infs + assert type(table.has_nulls) is bool + assert type(table.has_infs) is bool def test_null_to_pandas(self, q, pd): q('ty:2 5 6 7 8 9 10 11 12 13 14 16 17 18 19h') @@ -3430,6 +3509,8 @@ def test_has_null_and_has_inf(self, q): dic = q('flip ([]0w,9?1f;0n,9?1f)') assert dic.has_nulls assert dic.has_infs + assert type(dic.has_nulls) is bool + assert type(dic.has_infs) is bool def test_nested_dict(self, q): single_nested = { @@ -4222,6 +4303,9 @@ def compare_nulls(q_null, py_null): def test_infinites(kx, q, pa): import datetime + # Pandas >= 2.0 supports non-nanosecond resolution, so month/date/minute + # infinities (out of the nanosecond range) convert instead of overflowing. + pandas_gt1 = not pd.__version__.split('.')[0] == '1' def compare_infinites(q_infinite, py_infinite): assert type(q_infinite) == type(py_infinite) @@ -4248,7 +4332,7 @@ def compare_all_infinites(q_infinites, py_infinites, conv, ind=None, skip=None): py_positive_infinites = [math.inf, math.inf, math.inf, float('inf'), float('inf'), datetime.datetime(2262, 4, 11, 23, 47, 16, 854775), 2147484007, 2147494604, datetime.timedelta(106751, 16, 854775, 0, 47, 23), - datetime.timedelta(-3220, 4, 33138, 0, 5, 5), + datetime.timedelta(minutes=2147483647), datetime.timedelta(24855, 7, 0, 0, 14, 3), datetime.timedelta(24, 23, 647000, 0, 31, 20)] compare_all_infinites(positive_infinites, py_positive_infinites, 'py') @@ -4256,7 +4340,7 @@ def compare_all_infinites(q_infinites, py_infinites, conv, ind=None, skip=None): py_negative_infinites = [-math.inf, -math.inf, -math.inf, float('-inf'), float('-inf'), datetime.datetime(1707, 9, 22, 0, 12, 43, 145224), -2147483287, -2147472690, datetime.timedelta(-106752, 43, 145224, 0, 12), - datetime.timedelta(3219, 55, 966861, 0, 54, 18), + datetime.timedelta(minutes=-2147483647), datetime.timedelta(-24856, 53, 0, 0, 45, 20), datetime.timedelta(-25, 36, 353000, 0, 28, 3)] compare_all_infinites(negative_infinites, py_negative_infinites, 'py') @@ -4309,18 +4393,33 @@ def compare_all_infinites(q_infinites, py_infinites, conv, ind=None, skip=None): np.float64('-inf'), pd.Timestamp('1707-09-22 00:12:43.145224193'), None, None, pd.Timedelta(-9223372036854775807, 'ns'), None, pd.Timedelta(-2147483647, 's'), pd.Timedelta(-2147483647, 'ms')] + # month (6), date (7) and minute (9) exceed pandas' nanosecond range. On + # pandas >= 2.0 they convert (coarser resolution) and are verified below: + # month/date pinned via their (already verified) .np() datetime64 value, minute + # directly. On pandas 1.x these overflow, so they remain skipped. skip = [6, 7, 9] - # 6 7 9 Values out of range - Pandas constructors block them compare_all_infinites(positive_infinites, pd_positive_infinites, 'pd', skip=skip) compare_all_infinites(negative_infinites, pd_negative_infinites, 'pd', skip=skip) + if pandas_gt1: + for i in (6, 7): + assert positive_infinites[i].pd().to_datetime64() == np_positive_infinites[i] + assert negative_infinites[i].pd().to_datetime64() == np_negative_infinites[i] + assert positive_infinites[9].pd().to_numpy() == np_positive_infinites[9] + assert negative_infinites[9].pd().to_numpy() == np_negative_infinites[9] # infinites in Vectors .pd() - skip = [6, 7, 9] # 6 7 9 Values out of range - Pandas constructors block them + skip = [6, 7, 9] # verified below on pandas >= 2.0 (overflow on pandas 1.x) pd_infinites = [[x, y] for x, y in zip(pd_positive_infinites, pd_negative_infinites)] pd_infinites[5][0] = pd.Timestamp('1707-09-22T00:12:43.145224191') compare_all_infinites(infinites, pd_infinites, 'pd', ind=0, skip=skip) pd_infinites[5][1] = pd.Timestamp('1707-09-22 00:12:43.145224193') compare_all_infinites(infinites, pd_infinites, 'pd', ind=1, skip=skip) + if pandas_gt1: + for i in (6, 7): + assert infinites[i].pd()[0].to_datetime64() == np_infinites[i][0] + assert infinites[i].pd()[1].to_datetime64() == np_infinites[i][1] + assert infinites[9].pd()[0].to_numpy() == np_infinites[9][0] + assert infinites[9].pd()[1].to_numpy() == np_infinites[9][1] # Atom infinites .pa() pa_positive_infinites = [np.int16(32767), np.int32(2147483647), np.int64(9223372036854775807), @@ -4335,16 +4434,23 @@ def compare_all_infinites(q_infinites, py_infinites, conv, ind=None, skip=None): None, None, pd.Timedelta(-9223372036854775807, 'ns'), None, pd.Timedelta(-2147483647, 's'), pd.Timedelta(-2147483647, 'ms')] + # month (6), date (7) and minute (9) exceed pandas' nanosecond range; verified + # below on pandas >= 2.0 (month/date pinned via .np(), minute directly). On + # pandas 1.x these overflow, so they remain skipped. skip = [6, 7, 9] - # 6, 7, 9 .pa runs but creates out of bounds objects compare_all_infinites(positive_infinites, pa_positive_infinites, 'pa', skip=skip) compare_all_infinites(negative_infinites, pa_negative_infinites, 'pa', skip=skip) + if pandas_gt1: + for i in (6, 7): + assert positive_infinites[i].pa().to_datetime64() == np_positive_infinites[i] + assert negative_infinites[i].pa().to_datetime64() == np_negative_infinites[i] + assert positive_infinites[9].pa().to_numpy() == np_positive_infinites[9] + assert negative_infinites[9].pa().to_numpy() == np_negative_infinites[9] # infinites in Vectors .pa() - skip = [5, 6, 7, 9] - # 5, 6 pyarrow.lib.ArrowNotImplementedError: Unsupported datetime64 time unit - # 7 OverflowError: days=-2147472692; must have magnitude <= 999999999 - # 9 pyarrow.lib.ArrowNotImplementedError: Unsupported timedelta64 time unit + # timestamp (5) is verified below; month (6), date (7) and minute (9) raise and + # are asserted with pytest.raises after the value comparisons. + skip = [6, 7, 9] pa_infinites = [[x, y] for x, y in zip(pa_positive_infinites, pa_negative_infinites)] pa_infinites[0][0] = pa.array([32767], pa.int16())[0] pa_infinites[1][0] = pa.array([2147483647], pa.int32())[0] @@ -4361,12 +4467,22 @@ def compare_all_infinites(q_infinites, py_infinites, conv, ind=None, skip=None): pa_infinites[2][1] = pa.array([-9223372036854775807], pa.int64())[0] pa_infinites[3][1] = pa.array([np.float32('-inf')], pa.float32())[0] pa_infinites[4][1] = pa.array([np.float64('-inf')], pa.float64())[0] - pa_infinites[5][1] = pa.array([-9223372036854775809+946684800000000000], pa.timestamp('ns'))[0] + pa_infinites[5][1] = pa.array([np.datetime64('1707-09-22T00:12:43.145224193')], + pa.timestamp('ns'))[0] pa_infinites[8][1] = pa.array([-9223372036854775807], pa.duration('ns'))[0] pa_infinites[10][1] = pa.array([-2147483647], pa.duration('s'))[0] pa_infinites[11][1] = pa.array([-2147483647], pa.duration('ms'))[0] compare_all_infinites(infinites, pa_infinites, 'pa', ind=1, skip=skip) + # month (6) and minute (9) vector .pa() raise on an unsupported pyarrow unit; + # date (7) constructs but its out-of-range value raises when materialised. + with pytest.raises(pa.lib.ArrowNotImplementedError): + infinites[6].pa() + with pytest.raises(pa.lib.ArrowNotImplementedError): + infinites[9].pa() + with pytest.raises(OverflowError): + infinites[7].pa()[0].as_py() + # Conversions of nested K lists requires a license, We need to be able to call # __getitem__ on the list to get correctly typed Numpy arrays to use.