Repository: ausgerechnet/cwb-ccc (language: python)

Issue:
keywords() does not support multiple p_atts
- `keywords()` only works with single `p_atts`
refactor: Dump = class(Corpus)
right now, the workflow looks like this:

```{python}
germaparl_green_dump = germaparl.query_s_att(s_att="parliamentary_group", values={"GRUENE"}, name="green")
# germaparl_green_dump is a dump, with methods "keywords", "colocates", etc.
germaparl_green = germaparl.activate_subcorpus("green")
# germaparl_green is a corpus, with methods "query", etc.
germaparl_green_19_dump = germaparl_green.query_s_att(s_att="lp", values={"19"}, name="green_19")
# dump, with respective keywords
germaparl_green_19_kw = germaparl_green_19.keywords()
# actual subcorpus:
germaparl_green_19 = germaparl.activate_subcorpus("green_19")
```

the distinction between a dump and a corpus is superfluous, in other words: a dump should be queryable


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..905c8bb
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 160
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 50e592c..70f434b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 legacy/
 
+Pipfile.lock
+
 *.so
 
 *.pyc
diff --git a/Pipfile b/Pipfile
index 5732428..fdaadf7 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,24 +4,23 @@ url = "https://pypi.org/simple"
 verify_ssl = true
 
 [dev-packages]
-cython = "==0.29.30"
-pytest = "==7.2.0"
-pylint = "==2.13.9"
-pytest-cov = "==3.0.0"
-tabulate = "==0.8.9"
+cython = "==3.0.0"
+twine = "==4.0.2"
+setuptools = "==68.0.0"
+pytest = "==7.4.0"
+pytest-cov = "==4.1.0"
+pytest-benchmark = "4.0.0"
+pylint = "==2.17.5"
+sphinx = "==5.3.0"
+enthought-sphinx-theme = "==0.7.3"
+tabulate = "==0.9.0"
 pyperclip = "==1.8.2"
-enthought-sphinx-theme = "==0.7.1"
-sphinx = "==5.0.0"
-twine = "==3.7.1"
-setuptools = "==65.5.1"
-pytest-benchmark = "3.4.1"
-exceptiongroup = "*"
 
 [packages]
-wheel = ">=0.37.1"
-association-measures = ">=0.2.6"
-pandas = ">=1.1.5"
-numexpr = ">=2.7.1"
-Bottleneck = ">=1.3.4"
-unidecode = ">=1.3.4"
-pyyaml = ">=6.0"
+wheel = ">=0.41.1,<0.42"
+association-measures = ">=0.2.6,<0.3"
+pandas = ">=1.3.5,<2.0"
+numexpr = ">=2.8.5<3.0"
+Bottleneck = ">=1.3.7,<2.0"
+unidecode = ">=1.3.6,<2.0"
+pyyaml = ">=6.0.1,<7.0"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 29d0d0b..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,1208 +0,0 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "4ff2c7b07eb03114fc95a0b2843061ad4fd87477a7c1c7f95cf70692d32cde42"
-        },
-        "pipfile-spec": 6,
-        "requires": {},
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "association-measures": {
-            "hashes": [
-                "sha256:857e5092f5651360ab84118ff1289dbd2ec8de5ab65be1698847a8dc8ab9a88f"
-            ],
-            "index": "pypi",
-            "version": "==0.2.6"
-        },
-        "bottleneck": {
-            "hashes": [
-                "sha256:01dd0281ae2d077612a6504c748f75a6dcd2e84fda627f8310f62e26888b90fb",
-                "sha256:0def72d6eb98ec13e421d5adce330ee6a86dbdd44935243665a3915bcd859e7f",
-                "sha256:14019297aeac6b02d11b6c944807a91d88afd262b57b792c11b546050ef227e6",
-                "sha256:163101226eccb683e812c5e5164ceb1c7d6841c72a84e4335878bb95c8d1f137",
-                "sha256:1f0a8bd9174e7b208810a036d498c707c6a9f524371ba7c78116d3bc19ef1680",
-                "sha256:2541d627e7c393054a3473fd5d6956b67f8495a690c74da45913b17fa5c89d03",
-                "sha256:2637351f910c3fcba590b44a037de3330fa80499c11a23e83b3cc4ab00da4324",
-                "sha256:2e88ef4629f3416380e508b035a314ea0a22040b72383c5d4ace5b5894645564",
-                "sha256:326ecc624aa74bee1c71979ac9059b7ce88e126ee792bcd87fe59f7dcc67d39e",
-                "sha256:3d8cec523b4558c119fb6a6b1a7db780207584cfb7ad3835a8a834fd5cd3da9a",
-                "sha256:3f0518e94503f12fb1c68dded1ced193555d3d73a333a08ad54235ccf0e2b1fc",
-                "sha256:46a16198d3e30a650b879edfbc924f188c7dc4006733810600410831d51bb501",
-                "sha256:4790740c6ece6b0c01cd767b4cdc33c3d2041f472afc9dfb75f864e804895972",
-                "sha256:47ade120fc8baec6a72dd7b53c7eb039afa6dce3881db7e7fe4f935fb5775f99",
-                "sha256:4925d92ec0d5934b8a8cdfd4bea1933175f16c2521ab118b6dcbbb47b2f0d2a0",
-                "sha256:58e38b60b576733116e814652fa2b9e56a91f526602ada21efb2caf9a3620891",
-                "sha256:59056626ebc7eee3a3419d725c16ae88c7132af080f3fc2f2971571ced19a727",
-                "sha256:5f13765810589f377a359e336235bd22b6fe7a018ceaae4d3bacc1d82a51c7c9",
-                "sha256:61375181555b0c6c25b6d5c12c4abed055e4161b374b9b81fabbaf29caacf23a",
-                "sha256:658e92c8701dda70c6fdd0a84455b3bd6d7a0d659ff92e7c15c65d0eadf77944",
-                "sha256:78ed47e473c8adf832cbf1cd12a62c1a82683aaad8e62c212126c3b1fe18abfc",
-                "sha256:8422f50e6891bc235c4fcf930cbac7cd30fbe03959254993a4213dbf5ab9d1fd",
-                "sha256:8bfd7afd157f39c6525eb639559cca1017c78a9d296134371e913534c7ffce8c",
-                "sha256:8f9e9b9dbf93870dd66bccf166a46e6346c8959fb6f4572a0c83314097ac4d44",
-                "sha256:a3deb3a195bc0d34a4ce0697c141a051de0f2d6d7cd386747127dc66d505cec3",
-                "sha256:ab769592a8c53f2ce99370491638bd0aab57f04f5520ef01b3aa5019e659616a",
-                "sha256:abf3dd26b64b00fc93a25367584f29b27ba8f20c3ee795ced787936544eb1e74",
-                "sha256:bc15e2545d4282d6f2529597df1bd6e4c5f0c44296b3f8425bc835305bd943c9",
-                "sha256:c43426c66a220d59974d10bb79ebfaf330727b1164ce71634b7db34536a5bfa8",
-                "sha256:cbcc281366a91b0a1960ad0bd99b5bbe87b64d814abbbbf84a2ad614c3ac4caf",
-                "sha256:d3ecfc076836e867f880aee7c6f13b44d2cf47447567632a9c9a80e56637ba3b",
-                "sha256:d5b65a0660a1f8150fe0662ff623d297c8b799155edfbefa3286ee091e100f36",
-                "sha256:d7a4031cb10f9eb98cd5a0dfa9c40905bcd9dbdddc01e116ed592fda7e117eb3",
-                "sha256:e234cdbf46f06308eb37b82537954be7130d859af9fc6e73e6f4c7cc1cd8fe6e",
-                "sha256:f236784459f7f07516bfc3522d0976b511529f865a78373029f6c59b41277ca1",
-                "sha256:fd306632cf81a2fa91d1597157a0d708e956f17ea32a6c2a5e4250d1f2104294"
-            ],
-            "index": "pypi",
-            "version": "==1.3.6"
-        },
-        "numexpr": {
-            "hashes": [
-                "sha256:059546e8f6283ccdb47c683101a890844f667fa6d56258d48ae2ecf1b3875957",
-                "sha256:17ac9cfe6d0078c5fc06ba1c1bbd20b8783f28c6f475bbabd3cad53683075cab",
-                "sha256:3f039321d1c17962c33079987b675fb251b273dbec0f51aac0934e932446ccc3",
-                "sha256:5538b30199bfc68886d2be18fcef3abd11d9271767a7a69ff3688defe782800a",
-                "sha256:655d84eb09adfee3c09ecf4a89a512225da153fdb7de13c447404b7d0523a9a7",
-                "sha256:6931b1e9d4f629f43c14b21d44f3f77997298bea43790cfcdb4dd98804f90783",
-                "sha256:6c368aa35ae9b18840e78b05f929d3a7b3abccdba9630a878c7db74ca2368339",
-                "sha256:6ee9db7598dd4001138b482342b96d78110dd77cefc051ec75af3295604dde6a",
-                "sha256:77898fdf3da6bb96aa8a4759a8231d763a75d848b2f2e5c5279dad0b243c8dfe",
-                "sha256:7bca95f4473b444428061d4cda8e59ac564dc7dc6a1dea3015af9805c6bc2946",
-                "sha256:7d71add384adc9119568d7e9ffa8a35b195decae81e0abf54a2b7779852f0637",
-                "sha256:845a6aa0ed3e2a53239b89c1ebfa8cf052d3cc6e053c72805e8153300078c0b1",
-                "sha256:90f12cc851240f7911a47c91aaf223dba753e98e46dff3017282e633602e76a7",
-                "sha256:9400781553541f414f82eac056f2b4c965373650df9694286b9bd7e8d413f8d8",
-                "sha256:9e34931089a6bafc77aaae21f37ad6594b98aa1085bb8b45d5b3cd038c3c17d9",
-                "sha256:9f096d707290a6a00b6ffdaf581ee37331109fb7b6c8744e9ded7c779a48e517",
-                "sha256:a38664e699526cb1687aefd9069e2b5b9387da7feac4545de446141f1ef86f46",
-                "sha256:a6d2d7740ae83ba5f3531e83afc4b626daa71df1ef903970947903345c37bd03",
-                "sha256:a75967d46b6bd56455dd32da6285e5ffabe155d0ee61eef685bbfb8dafb2e484",
-                "sha256:b076db98ca65eeaf9bd224576e3ac84c05e451c0bd85b13664b7e5f7b62e2c70",
-                "sha256:b318541bf3d8326682ebada087ba0050549a16d8b3fa260dd2585d73a83d20a7",
-                "sha256:b96334fc1748e9ec4f93d5fadb1044089d73fb08208fdb8382ed77c893f0be01",
-                "sha256:c867cc36cf815a3ec9122029874e00d8fbcef65035c4a5901e9b120dd5d626a2",
-                "sha256:d5432537418d18691b9115d615d6daa17ee8275baef3edf1afbbf8bc69806147",
-                "sha256:db93cf1842f068247de631bfc8af20118bf1f9447cd929b531595a5e0efc9346",
-                "sha256:df35324666b693f13a016bc7957de7cc4d8801b746b81060b671bf78a52b9037",
-                "sha256:df3a1f6b24214a1ab826e9c1c99edf1686c8e307547a9aef33910d586f626d01",
-                "sha256:eaec59e9bf70ff05615c34a8b8d6c7bd042bd9f55465d7b495ea5436f45319d0",
-                "sha256:f3a920bfac2645017110b87ddbe364c9c7a742870a4d2f6120b8786c25dc6db3",
-                "sha256:ff5835e8af9a212e8480003d731aad1727aaea909926fd009e8ae6a1cba7f141"
-            ],
-            "index": "pypi",
-            "version": "==2.8.4"
-        },
-        "numpy": {
-            "hashes": [
-                "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9",
-                "sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398",
-                "sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7",
-                "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2",
-                "sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954",
-                "sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6",
-                "sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032",
-                "sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36",
-                "sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c",
-                "sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8",
-                "sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7",
-                "sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f",
-                "sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700",
-                "sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086",
-                "sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7",
-                "sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2",
-                "sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9",
-                "sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407",
-                "sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2",
-                "sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36",
-                "sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566",
-                "sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1",
-                "sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e",
-                "sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51",
-                "sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1",
-                "sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d",
-                "sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf",
-                "sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==1.24.1"
-        },
-        "pandas": {
-            "hashes": [
-                "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813",
-                "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792",
-                "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406",
-                "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373",
-                "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328",
-                "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996",
-                "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf",
-                "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6",
-                "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7",
-                "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc",
-                "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1",
-                "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23",
-                "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a",
-                "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51",
-                "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572",
-                "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31",
-                "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5",
-                "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a",
-                "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003",
-                "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d",
-                "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354",
-                "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee",
-                "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa",
-                "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0",
-                "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9",
-                "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae",
-                "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"
-            ],
-            "index": "pypi",
-            "version": "==1.5.3"
-        },
-        "python-dateutil": {
-            "hashes": [
-                "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
-                "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.8.2"
-        },
-        "pytz": {
-            "hashes": [
-                "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0",
-                "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"
-            ],
-            "version": "==2022.7.1"
-        },
-        "pyyaml": {
-            "hashes": [
-                "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf",
-                "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
-                "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
-                "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
-                "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
-                "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
-                "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
-                "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
-                "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
-                "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
-                "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
-                "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
-                "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782",
-                "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
-                "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
-                "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
-                "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
-                "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
-                "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1",
-                "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
-                "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
-                "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
-                "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
-                "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
-                "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
-                "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d",
-                "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
-                "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
-                "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7",
-                "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
-                "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
-                "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
-                "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358",
-                "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
-                "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
-                "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
-                "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
-                "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f",
-                "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
-                "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
-            ],
-            "index": "pypi",
-            "version": "==6.0"
-        },
-        "scipy": {
-            "hashes": [
-                "sha256:0490dc499fe23e4be35b8b6dd1e60a4a34f0c4adb30ac671e6332446b3cbbb5a",
-                "sha256:0ab2a58064836632e2cec31ca197d3695c86b066bc4818052b3f5381bfd2a728",
-                "sha256:151f066fe7d6653c3ffefd489497b8fa66d7316e3e0d0c0f7ff6acca1b802809",
-                "sha256:16ba05d3d1b9f2141004f3f36888e05894a525960b07f4c2bfc0456b955a00be",
-                "sha256:27e548276b5a88b51212b61f6dda49a24acf5d770dff940bd372b3f7ced8c6c2",
-                "sha256:2ad449db4e0820e4b42baccefc98ec772ad7818dcbc9e28b85aa05a536b0f1a2",
-                "sha256:2f9ea0a37aca111a407cb98aa4e8dfde6e5d9333bae06dfa5d938d14c80bb5c3",
-                "sha256:38bfbd18dcc69eeb589811e77fae552fa923067fdfbb2e171c9eac749885f210",
-                "sha256:3afcbddb4488ac950ce1147e7580178b333a29cd43524c689b2e3543a080a2c8",
-                "sha256:42ab8b9e7dc1ebe248e55f54eea5307b6ab15011a7883367af48dd781d1312e4",
-                "sha256:441cab2166607c82e6d7a8683779cb89ba0f475b983c7e4ab88f3668e268c143",
-                "sha256:4bd0e3278126bc882d10414436e58fa3f1eca0aa88b534fcbf80ed47e854f46c",
-                "sha256:4df25a28bd22c990b22129d3c637fd5c3be4b7c94f975dca909d8bab3309b694",
-                "sha256:5cd7a30970c29d9768a7164f564d1fbf2842bfc77b7d114a99bc32703ce0bf48",
-                "sha256:6e4497e5142f325a5423ff5fda2fff5b5d953da028637ff7c704378c8c284ea7",
-                "sha256:6faf86ef7717891195ae0537e48da7524d30bc3b828b30c9b115d04ea42f076f",
-                "sha256:954ff69d2d1bf666b794c1d7216e0a746c9d9289096a64ab3355a17c7c59db54",
-                "sha256:9b878c671655864af59c108c20e4da1e796154bd78c0ed6bb02bc41c84625686",
-                "sha256:b901b423c91281a974f6cd1c36f5c6c523e665b5a6d5e80fcb2334e14670eefd",
-                "sha256:c8b3cbc636a87a89b770c6afc999baa6bcbb01691b5ccbbc1b1791c7c0a07540",
-                "sha256:e096b062d2efdea57f972d232358cb068413dc54eec4f24158bcbb5cb8bddfd8"
-            ],
-            "markers": "python_version < '3.12' and python_version >= '3.8'",
-            "version": "==1.10.0"
-        },
-        "six": {
-            "hashes": [
-                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
-                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.16.0"
-        },
-        "unidecode": {
-            "hashes": [
-                "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be",
-                "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"
-            ],
-            "index": "pypi",
-            "version": "==1.3.6"
-        },
-        "wheel": {
-            "hashes": [
-                "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac",
-                "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"
-            ],
-            "index": "pypi",
-            "version": "==0.38.4"
-        }
-    },
-    "develop": {
-        "alabaster": {
-            "hashes": [
-                "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3",
-                "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.7.13"
-        },
-        "astroid": {
-            "hashes": [
-                "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b",
-                "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"
-            ],
-            "markers": "python_full_version >= '3.6.2'",
-            "version": "==2.11.7"
-        },
-        "attrs": {
-            "hashes": [
-                "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
-                "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==22.2.0"
-        },
-        "babel": {
-            "hashes": [
-                "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe",
-                "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.11.0"
-        },
-        "bleach": {
-            "hashes": [
-                "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414",
-                "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==6.0.0"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
-                "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2022.12.7"
-        },
-        "cffi": {
-            "hashes": [
-                "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
-                "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
-                "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
-                "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
-                "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
-                "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
-                "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
-                "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
-                "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
-                "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
-                "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
-                "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
-                "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
-                "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
-                "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
-                "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
-                "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
-                "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
-                "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
-                "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
-                "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
-                "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
-                "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
-                "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
-                "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
-                "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
-                "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
-                "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
-                "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
-                "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
-                "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
-                "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
-                "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
-                "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
-                "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
-                "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
-                "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
-                "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
-                "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
-                "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
-                "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
-                "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
-                "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
-                "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
-                "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
-                "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
-                "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
-                "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
-                "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
-                "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
-                "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
-                "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
-                "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
-                "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
-                "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
-                "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
-                "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
-                "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
-                "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
-                "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
-                "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
-                "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
-                "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
-                "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
-            ],
-            "version": "==1.15.1"
-        },
-        "charset-normalizer": {
-            "hashes": [
-                "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b",
-                "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42",
-                "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d",
-                "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b",
-                "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a",
-                "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59",
-                "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154",
-                "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1",
-                "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c",
-                "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a",
-                "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d",
-                "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6",
-                "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b",
-                "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b",
-                "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783",
-                "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5",
-                "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918",
-                "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555",
-                "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639",
-                "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786",
-                "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e",
-                "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed",
-                "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820",
-                "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8",
-                "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3",
-                "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541",
-                "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14",
-                "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be",
-                "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e",
-                "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76",
-                "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b",
-                "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c",
-                "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b",
-                "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3",
-                "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc",
-                "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6",
-                "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59",
-                "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4",
-                "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d",
-                "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d",
-                "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3",
-                "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a",
-                "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea",
-                "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6",
-                "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e",
-                "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603",
-                "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24",
-                "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a",
-                "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58",
-                "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678",
-                "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a",
-                "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c",
-                "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6",
-                "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18",
-                "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174",
-                "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317",
-                "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f",
-                "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc",
-                "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837",
-                "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41",
-                "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c",
-                "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579",
-                "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753",
-                "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8",
-                "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291",
-                "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087",
-                "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866",
-                "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3",
-                "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d",
-                "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1",
-                "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca",
-                "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e",
-                "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db",
-                "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72",
-                "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d",
-                "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc",
-                "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539",
-                "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d",
-                "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af",
-                "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b",
-                "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602",
-                "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f",
-                "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478",
-                "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c",
-                "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e",
-                "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479",
-                "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7",
-                "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.0.1"
-        },
-        "colorama": {
-            "hashes": [
-                "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
-                "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
-            "version": "==0.4.6"
-        },
-        "coverage": {
-            "extras": [
-                "toml"
-            ],
-            "hashes": [
-                "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab",
-                "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851",
-                "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265",
-                "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0",
-                "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a",
-                "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5",
-                "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6",
-                "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311",
-                "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada",
-                "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f",
-                "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8",
-                "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc",
-                "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73",
-                "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf",
-                "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e",
-                "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352",
-                "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c",
-                "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c",
-                "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c",
-                "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda",
-                "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d",
-                "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0",
-                "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3",
-                "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d",
-                "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038",
-                "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c",
-                "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8",
-                "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa",
-                "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09",
-                "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b",
-                "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c",
-                "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a",
-                "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52",
-                "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3",
-                "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146",
-                "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a",
-                "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f",
-                "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4",
-                "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c",
-                "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75",
-                "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040",
-                "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063",
-                "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050",
-                "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7",
-                "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222",
-                "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912",
-                "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801",
-                "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d",
-                "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06",
-                "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8",
-                "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==7.1.0"
-        },
-        "cryptography": {
-            "hashes": [
-                "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b",
-                "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f",
-                "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190",
-                "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f",
-                "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f",
-                "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb",
-                "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c",
-                "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773",
-                "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72",
-                "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8",
-                "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717",
-                "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9",
-                "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856",
-                "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96",
-                "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288",
-                "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39",
-                "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e",
-                "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce",
-                "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1",
-                "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de",
-                "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df",
-                "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf",
-                "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==39.0.0"
-        },
-        "cython": {
-            "hashes": [
-                "sha256:019d330ac580b2ca4a457c464ac0b8c35009d820ef5d09f328d6e31a10e1ce89",
-                "sha256:0b83a342a071c4f14e7410568e0c0bd95e2f20c0b32944e3a721649a1357fda4",
-                "sha256:0cd6c932e945af15ae4ddcf8fdc0532bda48784c92ed0a53cf4fae897067ccd1",
-                "sha256:1e078943bbde703ca08d43e719480eb8b187d9023cbd91798619f5b5e18d0d71",
-                "sha256:20778297c8bcba201ca122a2f792a9899d6e64c68a92363dd7eb24306d54d7ce",
-                "sha256:2235b62da8fe6fa8b99422c8e583f2fb95e143867d337b5c75e4b9a1a865f9e3",
-                "sha256:28db751e2d8365b39664d9cb62dc1668688b8fcc5b954e9ca9d20e0b8e03d8b0",
-                "sha256:3993aafd68a7311ef94e00e44a137f6a50a69af0575ebcc8a0a074ad4152a2b2",
-                "sha256:3d0239c7a22a0f3fb1deec75cab0078eba4dd17868aa992a54a178851e0c8684",
-                "sha256:5183356c756b56c2df12d96300d602e47ffb89943c5a0bded66faca5d3da7be0",
-                "sha256:58d2b734250c1093bc69c1c3a6f5736493b9f8b34eb765f0a28a4a09468c0b00",
-                "sha256:5a8a3709ad9343a1dc02b8ec9cf6bb284be248d2c64af85464d9c3525eec74a5",
-                "sha256:5c7cfd908efc77306ddd41ef07f5a7a352c9205ced5c1e00a0e5ece4391707c4",
-                "sha256:5f2dae7dd56860018d5fd5032a71f11fdc224020932b463d0511a1536f27df85",
-                "sha256:60d370c33d56077d30e5f425026e58c2559e93b4784106f61581cf54071f6270",
-                "sha256:6b389a94b42909ff56d3491fde7c44802053a103701a7d210dcdd449a5b4f7b4",
-                "sha256:71fd1d910aced510c001936667fc7f2901c49b2ca7a2ad67358979c94a7f42ac",
-                "sha256:786ee7b0cdb508b6de64c0f1f9c74f207186dfafad1ef938f25b7494cc481a80",
-                "sha256:7eff71c39b98078deaad1d1bdbf10864d234e2ab5d5257e980a6926a8523f697",
-                "sha256:80a7255ad84620f53235c0720cdee2bc7431d9e3db7b3742823a606c329eb539",
-                "sha256:88c5e2f92f16cd999ddfc43d572639679e8a057587088e627e98118e46a803e6",
-                "sha256:8e08f18d249b9b65e272a5a60f3360a8922c4c149036b98fc821fe1afad5bdae",
-                "sha256:9462e9cf284d9b1d2c5b53d62188e3c09cc5c7a0018ba349d99b73cf930238de",
-                "sha256:9826981308802c61a76f967875b31b7c683b7fc369eabaa6cbc22efeb12c90e8",
-                "sha256:9f1fe924c920b699af27aefebd722df4cfbb85206291623cd37d1a7ddfd57792",
-                "sha256:a30092c6e2d24255fbfe0525f9a750554f96a263ed986d12ac3c9f7d9a85a424",
-                "sha256:abcaf99f90cddc0f53600613eaafc81d27c4ac0671f0df8bce5466d4e86d54a1",
-                "sha256:acb72e0b42079862cf2f894964b41f261e941e75677e902c5f4304b3eb00af33",
-                "sha256:b17639b6a155abaa61a89f6f1323fb57b138d0529911ca03978d594945d062ba",
-                "sha256:c299c5b250ae9f81c38200441b6f1d023aeee9d8e7f61c04001c7437181ccb06",
-                "sha256:c79685dd4631a188e2385dc6a232896c7b67ea2e3e5f8b5555b4b743f475d6d7",
-                "sha256:d0859a958e0155b6ae4dee04170ccfac2c3d613a7e3bee8749614530b9e3b4a4",
-                "sha256:d0f34b44078e3e0b2f1be2b99044619b37127128e7d55c54bbd2438adcaf31d3",
-                "sha256:d166d9f853db436f5e10733a9bd615699ddb4238feadcbdf5ae50dc0b18b18f5",
-                "sha256:d52d5733dcb144deca8985f0a197c19cf71e6bd6bd9d8034f3f67b2dea68d12b",
-                "sha256:e29d3487f357108b711f2f29319811d92166643d29aec1b8e063aad46a346775",
-                "sha256:e36755e71fd20eceb410cc441b7f2586654c2edb013f4663842fdaf60b96c1ca",
-                "sha256:e5cb144728a335d7a7fd0a61dff6abb7a9aeff9acd46d50b886b7d9a95bb7311",
-                "sha256:e605635a92ae862cb46d84d1d6883324518f9aaff4a71cede6d61df20b6a410c",
-                "sha256:ffa8c09617833ff0824aa7926fa4fa9d2ec3929c67168e89105f276b7f36a63e"
-            ],
-            "index": "pypi",
-            "version": "==0.29.30"
-        },
-        "dill": {
-            "hashes": [
-                "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
-                "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==0.3.6"
-        },
-        "docutils": {
-            "hashes": [
-                "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
-                "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==0.18.1"
-        },
-        "enthought-sphinx-theme": {
-            "hashes": [
-                "sha256:0a0d1f5cf334fba7fe947cd6f48814696ed2c851dd8172880403b7db3dfee448",
-                "sha256:9e1116b6b1b0baf2a8adb4d1481ead56e5f57b52f7b98ef63e528b5b0bf74ef2"
-            ],
-            "index": "pypi",
-            "version": "==0.7.1"
-        },
-        "exceptiongroup": {
-            "hashes": [
-                "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e",
-                "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"
-            ],
-            "index": "pypi",
-            "version": "==1.1.0"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
-                "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==3.4"
-        },
-        "imagesize": {
-            "hashes": [
-                "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b",
-                "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.4.1"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad",
-                "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"
-            ],
-            "markers": "python_version < '3.10'",
-            "version": "==6.0.0"
-        },
-        "iniconfig": {
-            "hashes": [
-                "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
-                "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.0.0"
-        },
-        "isort": {
-            "hashes": [
-                "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6",
-                "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"
-            ],
-            "markers": "python_full_version >= '3.7.0'",
-            "version": "==5.11.4"
-        },
-        "jaraco.classes": {
-            "hashes": [
-                "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158",
-                "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.2.3"
-        },
-        "jeepney": {
-            "hashes": [
-                "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806",
-                "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==0.8.0"
-        },
-        "jinja2": {
-            "hashes": [
-                "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
-                "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.1.2"
-        },
-        "keyring": {
-            "hashes": [
-                "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd",
-                "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==23.13.1"
-        },
-        "lazy-object-proxy": {
-            "hashes": [
-                "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
-                "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
-                "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
-                "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
-                "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
-                "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
-                "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
-                "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
-                "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
-                "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
-                "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
-                "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
-                "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
-                "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
-                "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
-                "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
-                "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
-                "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
-                "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
-                "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
-                "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
-                "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
-                "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
-                "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
-                "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
-                "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
-                "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
-                "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
-                "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
-                "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
-                "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
-                "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
-                "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
-                "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
-                "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
-                "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.9.0"
-        },
-        "markupsafe": {
-            "hashes": [
-                "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
-                "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
-                "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
-                "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
-                "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
-                "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
-                "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
-                "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
-                "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
-                "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
-                "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
-                "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
-                "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
-                "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
-                "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
-                "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
-                "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
-                "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
-                "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
-                "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
-                "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
-                "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
-                "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
-                "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
-                "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
-                "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
-                "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
-                "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
-                "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
-                "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
-                "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
-                "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
-                "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
-                "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
-                "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
-                "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
-                "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
-                "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
-                "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
-                "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
-                "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
-                "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
-                "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
-                "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
-                "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
-                "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
-                "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
-                "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
-                "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
-                "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.1.2"
-        },
-        "mccabe": {
-            "hashes": [
-                "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
-                "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==0.7.0"
-        },
-        "more-itertools": {
-            "hashes": [
-                "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41",
-                "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==9.0.0"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
-                "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==23.0"
-        },
-        "pkginfo": {
-            "hashes": [
-                "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546",
-                "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.9.6"
-        },
-        "platformdirs": {
-            "hashes": [
-                "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
-                "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.6.2"
-        },
-        "pluggy": {
-            "hashes": [
-                "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
-                "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.0.0"
-        },
-        "py-cpuinfo": {
-            "hashes": [
-                "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690",
-                "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"
-            ],
-            "version": "==9.0.0"
-        },
-        "pycparser": {
-            "hashes": [
-                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
-                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
-            ],
-            "version": "==2.21"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297",
-                "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.14.0"
-        },
-        "pylint": {
-            "hashes": [
-                "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731",
-                "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"
-            ],
-            "index": "pypi",
-            "version": "==2.13.9"
-        },
-        "pyperclip": {
-            "hashes": [
-                "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"
-            ],
-            "index": "pypi",
-            "version": "==1.8.2"
-        },
-        "pytest": {
-            "hashes": [
-                "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
-                "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
-            ],
-            "index": "pypi",
-            "version": "==7.2.0"
-        },
-        "pytest-benchmark": {
-            "hashes": [
-                "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1",
-                "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"
-            ],
-            "index": "pypi",
-            "version": "==4.0.0"
-        },
-        "pytest-cov": {
-            "hashes": [
-                "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
-                "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
-            ],
-            "index": "pypi",
-            "version": "==3.0.0"
-        },
-        "pytz": {
-            "hashes": [
-                "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0",
-                "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"
-            ],
-            "version": "==2022.7.1"
-        },
-        "readme-renderer": {
-            "hashes": [
-                "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273",
-                "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==37.3"
-        },
-        "requests": {
-            "hashes": [
-                "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa",
-                "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"
-            ],
-            "markers": "python_version >= '3.7' and python_version < '4'",
-            "version": "==2.28.2"
-        },
-        "requests-toolbelt": {
-            "hashes": [
-                "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7",
-                "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.10.1"
-        },
-        "rfc3986": {
-            "hashes": [
-                "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd",
-                "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.0.0"
-        },
-        "secretstorage": {
-            "hashes": [
-                "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77",
-                "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"
-            ],
-            "markers": "sys_platform == 'linux'",
-            "version": "==3.3.3"
-        },
-        "setuptools": {
-            "hashes": [
-                "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31",
-                "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"
-            ],
-            "index": "pypi",
-            "version": "==65.5.1"
-        },
-        "six": {
-            "hashes": [
-                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
-                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.16.0"
-        },
-        "snowballstemmer": {
-            "hashes": [
-                "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1",
-                "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"
-            ],
-            "version": "==2.2.0"
-        },
-        "sphinx": {
-            "hashes": [
-                "sha256:464d9c1bd5613bcebe76b46658763f3f3dbb184da7406e632a84596d3cd8ee90",
-                "sha256:af248b21e3282f847ff20feebe7a1985fb34773cbe3fc75bf206897f1a2199c4"
-            ],
-            "index": "pypi",
-            "version": "==5.0.0"
-        },
-        "sphinxcontrib-applehelp": {
-            "hashes": [
-                "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228",
-                "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==1.0.4"
-        },
-        "sphinxcontrib-devhelp": {
-            "hashes": [
-                "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
-                "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.2"
-        },
-        "sphinxcontrib-htmlhelp": {
-            "hashes": [
-                "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07",
-                "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2.0.0"
-        },
-        "sphinxcontrib-jsmath": {
-            "hashes": [
-                "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
-                "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.1"
-        },
-        "sphinxcontrib-qthelp": {
-            "hashes": [
-                "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
-                "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.0.3"
-        },
-        "sphinxcontrib-serializinghtml": {
-            "hashes": [
-                "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd",
-                "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.1.5"
-        },
-        "tabulate": {
-            "hashes": [
-                "sha256:d7c013fe7abbc5e491394e10fa845f8f32fe54f8dc60c6622c6cf482d25d47e4",
-                "sha256:eb1d13f25760052e8931f2ef80aaf6045a6cceb47514db8beab24cded16f13a7"
-            ],
-            "index": "pypi",
-            "version": "==0.8.9"
-        },
-        "tomli": {
-            "hashes": [
-                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
-                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
-            ],
-            "markers": "python_version < '3.11'",
-            "version": "==2.0.1"
-        },
-        "tqdm": {
-            "hashes": [
-                "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4",
-                "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==4.64.1"
-        },
-        "twine": {
-            "hashes": [
-                "sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8",
-                "sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345"
-            ],
-            "index": "pypi",
-            "version": "==3.7.1"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
-                "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
-            ],
-            "markers": "python_version < '3.10'",
-            "version": "==4.4.0"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72",
-                "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==1.26.14"
-        },
-        "webencodings": {
-            "hashes": [
-                "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
-                "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
-            ],
-            "version": "==0.5.1"
-        },
-        "wrapt": {
-            "hashes": [
-                "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3",
-                "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b",
-                "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4",
-                "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2",
-                "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656",
-                "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3",
-                "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff",
-                "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310",
-                "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a",
-                "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57",
-                "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069",
-                "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383",
-                "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe",
-                "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87",
-                "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d",
-                "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b",
-                "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907",
-                "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f",
-                "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0",
-                "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28",
-                "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1",
-                "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853",
-                "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc",
-                "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3",
-                "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3",
-                "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164",
-                "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1",
-                "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c",
-                "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1",
-                "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7",
-                "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1",
-                "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320",
-                "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed",
-                "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1",
-                "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248",
-                "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c",
-                "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456",
-                "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77",
-                "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef",
-                "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1",
-                "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7",
-                "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86",
-                "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4",
-                "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d",
-                "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d",
-                "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8",
-                "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5",
-                "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471",
-                "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00",
-                "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68",
-                "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3",
-                "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d",
-                "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735",
-                "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d",
-                "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569",
-                "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7",
-                "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59",
-                "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5",
-                "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb",
-                "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b",
-                "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f",
-                "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462",
-                "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015",
-                "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.14.1"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa",
-                "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.11.0"
-        }
-    }
-}
diff --git a/README.md b/README.md
index fb0d83a..8a74d67 100644
--- a/README.md
+++ b/README.md
@@ -5,14 +5,14 @@
 [![License](https://img.shields.io/pypi/l/cwb-ccc.svg)](https://github.com/ausgerechnet/cwb-ccc/blob/master/LICENSE)
 [![Imports: association-measures](https://img.shields.io/badge/%20imports-association--measures-%231674b1?style=flat&labelColor=gray)](https://github.com/fau-klue/pandas-association-measures)
 
-**cwb-ccc** is a Python 3 wrapper around the [IMS Open Corpus Workbench (CWB)](http://cwb.sourceforge.net/).  Main purpose of the module is to run queries, extract concordance lines, and score frequency lists (particularly to extract collocates and keywords).
+**cwb-ccc** is a Python 3 wrapper around the [IMS Open Corpus Workbench (CWB)](http://cwb.sourceforge.net/).  Main purpose of the module is to run queries (including queries with more than two anchor points), extract concordance lines, and score frequency lists (particularly to extract collocates and keywords).
 
 The [Quickstart](#quickstart) should get you started. For a more detailed overview of the functionality, see the [Vignette](demos/vignette.md).
 
 * [Installation](#installation)
 * [Quickstart](#quickstart)
   * [Accessing Corpora](#accessing-corpora)
-  * [Queries and Dumps](#queries-and-dumps)
+  * [Queries and SubCorpora](#queries-and-subcorpus)
   * [Concordancing](#concordancing)
   * [Collocates](#collocation-analyses)
   * [Keywords](#keyword-analyses)
@@ -24,7 +24,7 @@ The [Quickstart](#quickstart) should get you started. For a more detailed overvi
 
 The module needs a working installation of the [CWB](http://cwb.sourceforge.net/) and operates on CWB-indexed corpora. If you want to run queries with more than two anchor points, you will need CWB version 3.4.16 or later.
 
-You can install this module with pip from [PyPI](https://pypi.org/project/cwb-ccc/):
+You can install cwb-ccc with pip from [PyPI](https://pypi.org/project/cwb-ccc/):
 
     python -m pip install cwb-ccc
 
@@ -32,6 +32,7 @@ You can also clone the source from [github](https://github.com/ausgerechnet/cwb-
 
     python -m pip install pipenv
     pipenv install --dev
+    pipenv run python3 -m cython -2 ccc/cl.pyx
     pipenv run python3 setup.py bdist_wheel
 
 
@@ -43,7 +44,7 @@ To list all available corpora, you can use
 ```python
 from ccc import Corpora
 
-corpora = Corpora(registry_path="/usr/local/share/cwb/registry/")
+corpora = Corpora(registry_dir="/usr/local/share/cwb/registry/")
 ```
 
 All further methods rely on the `Corpus` class, which establishes the connection to your CWB-indexed corpus:
@@ -53,29 +54,30 @@ from ccc import Corpus
 
 corpus = Corpus(
   corpus_name="GERMAPARL1386",
-  registry_path="/usr/local/share/cwb/registry/"
+  registry_dir="/usr/local/share/cwb/registry/"
 )
 ```
 
 This will raise a `KeyError` if the named corpus is not in the specified registry.
 
 
-### Queries and Dumps ###
+### Queries and SubCorpora ###
+
 The usual starting point for using this module is to run a query with `corpus.query()`, which accepts valid CQP queries such as
 
 ```python
-dump = corpus.query('[lemma="Arbeit"]', context_break='s')
+subcorpus = corpus.query('[lemma="Arbeit"]', context_break='s')
 ```
 
-The result is a `Dump` object; at its core is a pandas DataFrame with corpus positions (similar to CWB dumps).
+The result is a `SubCorpus`; at its core this is a pandas DataFrame with corpus positions (similar to CWB dumps of NQRs).
 
 
 ### Concordancing ###
 
-You can access concordance lines via the `concordance()` method of the dump.  This method returns a DataFrame with information about the query matches in context:
+You can access concordance lines via the `concordance()` method of the subcorpus.  This method returns a DataFrame with information about the query matches in context:
 
 <details>
-<summary><code>dump.concordance()</code></summary>
+<summary><code>subcorpus.concordance()</code></summary>
 <p>
 
 | *match* | *matchend* | word                                                                                                                                                    |
@@ -92,10 +94,10 @@ You can access concordance lines via the `concordance()` method of the dump.  Th
 </details>
 <br/>
 
-This retrieves concordance lines in simple format in the order in which they appear in the corpus. A better approach is
+By default, this retrieves concordance lines in simple format in the order in which they appear in the corpus. A better approach is
 
 <details>
-<summary><code>dump.concordance(form='kwic', order='random')</code></summary>
+<summary><code>subcorpus.concordance(form='kwic', order='random')</code></summary>
 <p>
 
 | *match* | *matchend* | left\_word                                                                                                                                    | node\_word | right\_word                                                                                                                                        |
@@ -114,11 +116,11 @@ This retrieves concordance lines in simple format in the order in which they app
 
 
 ### Collocation Analyses ###
-After executing a query, you can use `dump.collocates()` to extract collocates for a given window size (symmetric windows around the corpus matches). The result will be a `DataFrame` with lemmata as index and frequency signatures and association measures as columns:
 
+After executing a query, you can use `subcorpus.collocates()` to extract collocates (see the vignette for parameter settings). The result is a `DataFrame` with lemmata as index and frequency signatures and association measures as columns:
 
 <details>
-<summary><code>dump.collocates()</code></summary>
+<summary><code>subcorpus.collocates()</code></summary>
 <p>
 
 | *item*   |   O11 |   O12 |   O21 |    O22 |   R1 |     R2 |   C1 |     C2 |      N |      E11 |     E12 |      E21 |    E22 |   z\_score |   t\_score |   log\_likelihood |   simple\_ll |   min\_sensitivity |   liddell |     dice |   log\_ratio |   conservative\_log\_ratio |   mutual\_information |   local\_mutual\_information |     ipm |   ipm\_reference |   ipm\_expected |   in\_nodes |   marginal |
@@ -136,17 +138,19 @@ After executing a query, you can use `dump.collocates()` to extract collocates f
 
 The dataframe contains the counts and is annotated with all available association measures in the [pandas-association-measures](https://pypi.org/project/association-measures/) package (parameter `ams`).
 
+
 ### Keyword Analyses ###
 
-Having created a subcorpus (a `dump`)
+Having created a subcorpus
+
 ```python
-dump = corpus.query(s_query='text_party', s_values={'CDU', 'CSU'})
+subcorpus = corpus.query(s_query='text_party', s_values={'CDU', 'CSU'})
 ```
 
 you can use its `keywords()` method for retrieving keywords:
 
 <details>
-<summary><code>dump.keywords(order='conservative_log_ratio')</code></summary>
+<summary><code>subcorpus.keywords(order='conservative_log_ratio')</code></summary>
 <p>
 
 | *item*     | O11 |   O12 |  O21 |    O22 |    R1 |     R2 |   C1 |     C2 |      N |     E11 |     E12 |     E21 |    E22 | z\_score | t\_score | log\_likelihood | simple\_ll | min\_sensitivity |  liddell |     dice | log\_ratio | conservative\_log\_ratio | mutual\_information | local\_mutual\_information |     ipm | ipm\_reference | ipm\_expected |
@@ -166,9 +170,7 @@ Just as with collocates, the result is a `DataFrame` with lemmata as index and f
 
 
 ## Testing ##
-The module ships with a small test corpus ("GERMAPARL1386"), which contains all speeches of the 86th session of the 13th German Bundestag on Feburary 8, 1996.
-
-The corpus consists of 149,800 tokens in 7332 paragraphs (s-attribute "p" with annotation "type" ("regular" or "interjection")) split into 11,364 sentences (s-attribute "s").  The p-attributes are "pos" and "lemma":
+The module ships with a small test corpus ("GERMAPARL1386"), which contains all speeches of the 86th session of the 13th German Bundestag on Feburary 8, 1996. This corpus consists of 149,800 tokens in 7332 paragraphs (s-attribute "p" with annotation "type" ("regular" or "interjection")) split into 11,364 sentences (s-attribute "s").  The p-attributes are "pos" and "lemma":
 
 <details>
 <summary><code>corpus.attributes_available</code></summary>
@@ -224,4 +226,4 @@ You can then simply
 - Special thanks to **Markus Opolka** for the original implementation of [association-measures](https://github.com/fau-klue/pandas-association-measures) and for forcing me to write tests.
 - The test corpus was extracted from the [GermaParl](https://github.com/PolMine/GermaParlTEI) corpus (see the [PolMine Project](https://polmine.github.io/)); many thanks to **Andreas Blätte**.
 - This work was supported by the [Emerging Fields Initiative (EFI)](https://www.fau.eu/research/collaborative-research/emerging-fields-initiative/) of [**Friedrich-Alexander-Universität Erlangen-Nürnberg**](https://www.fau.eu/), project title [Exploring the *Fukushima Effect*](https://www.linguistik.phil.fau.de/projects/efe/) (2017-2020).
-- Further development of the package is funded by the Deutsche Forschungsgemeinschaft (DFG) within the project [Reconstructing Arguments from Noisy Text](https://www.linguistik.phil.fau.de/projects/rant/), grant number 377333057 (2018-2023), as part of the Priority Program [**Robust Argumentation Machines**](http://www.spp-ratio.de/) (SPP-1999).
+- Further development of the package is funded by the Deutsche Forschungsgemeinschaft (DFG) within the project [Reconstructing Arguments from Noisy Text](https://www.linguistik.phil.fau.de/projects/rant/), grant number 377333057 (2018-2024), as part of the Priority Program [**Robust Argumentation Machines**](http://www.spp-ratio.de/) (SPP-1999).
diff --git a/ccc-utils.R b/ccc-utils.R
index 0529500..c3ef841 100644
--- a/ccc-utils.R
+++ b/ccc-utils.R
@@ -4,33 +4,26 @@ library(kableExtra)
 library(DT)
 library(colorspace)
 library(matrixStats)
-library(anomalize)
-library(gespeR)
+# library(anomalize)
+# library(gespeR)
 library(irr)
 library(gridExtra)
+library(ggrepel)
+
+options(dplyr.summarise.inform = FALSE)
 
 # function for formatting concordance lines
-concordance.format <- function(conc, n = 10, tex = F, crop=NULL){
-  
-  if (tex){
-    conc %>% 
-      head(n) %>% 
-      xtable::xtable() %>% 
-      print(booktabs = TRUE, hline.after = 0:n)
-  }
-  
-  else {
-    conc %>%
-      head(n) %>%
-      select(c("left_word", "node_word", "right_word")) %>%
-      kbl(booktabs = T, align = c("rcl"), longtable = T,
-          col.names = c("left context", "node", "right context"),
-          table.attr = "style = \"color: white; background-color: black;\"") %>%
-      row_spec(0, bold = T) %>%
-      column_spec(c(1, 3), width = "6cm") %>%
-      column_spec(2, bold = T, width = "3cm") %>%
-      kable_styling(latex_options = "striped")
-  }
+concordance.format <- function(conc, n = 10, crop=NULL, columns = c("left_word", "node_word", "right_word")){
+  conc %>%
+    head(n) %>%
+    select(columns) %>%
+    kbl(booktabs = T, align = c("rcl"), longtable = T,
+        # col.names = c("left context", "node", "right context"),
+        table.attr = "style = \"color: white; background-color: black;\"") %>%
+    row_spec(0, bold = T) %>%
+    column_spec(c(1, 3), width = "6cm") %>%
+    column_spec(2, bold = T, width = "3cm") %>%
+    kable_styling(latex_options = "striped")
 }
 
 # function for plotting collocates
diff --git a/ccc/__init__.py b/ccc/__init__.py
index ac3ce50..cbd1ed1 100644
--- a/ccc/__init__.py
+++ b/ccc/__init__.py
@@ -4,5 +4,5 @@
 
 """
 
-from .cwb import Corpora, Corpus
+from .cwb import Corpora, Corpus, SubCorpus
 from .version import __version__
diff --git a/ccc/cache.py b/ccc/cache.py
index 11cc757..018ca74 100644
--- a/ccc/cache.py
+++ b/ccc/cache.py
@@ -23,12 +23,13 @@ def generate_idx(identifiers, prefix='', length=10):
     return prefix + identifier[:length]
 
 
-def generate_library_idx(lib_path, prefix='', length=10):
-    """generate an ID wordlists and macros (subdirectories of lib_path)
+def generate_library_idx(lib_dir, prefix='', length=10):
+    """generate an ID for a library,
+    i.e. all files ending on '.txt' in "wordlists" and "macros" of lib_dir
 
     """
-    wordlists = glob(os.path.join(lib_path, 'wordlists', '*.txt'))
-    macros = glob(os.path.join(lib_path, 'macros', '*.txt'))
+    wordlists = glob(os.path.join(lib_dir, 'wordlists', '*.txt'))
+    macros = glob(os.path.join(lib_dir, 'macros', '*.txt'))
     paths = macros + wordlists
     identifiers = [
         generate_idx(open(p, 'rt', encoding='utf-8')) for p in sorted(paths)
@@ -37,7 +38,7 @@ def generate_library_idx(lib_path, prefix='', length=10):
 
 
 class Cache:
-    
+
     def __init__(self, path=None):
 
         self.path = path
@@ -81,7 +82,7 @@ def get(self, identifier):
     def set(self, identifier, value):
 
         if self.path is None:
-            logger.info('no cache path')
+            logger.error('no cache path')
             return
 
         if isinstance(identifier, str):
diff --git a/ccc/collocates.py b/ccc/collocates.py
index 482c381..c4775ef 100644
--- a/ccc/collocates.py
+++ b/ccc/collocates.py
@@ -12,6 +12,7 @@
 from pandas import DataFrame
 
 # part of module
+from .cache import generate_idx
 from .counts import score_counts
 from .utils import node2cotext
 
@@ -31,36 +32,52 @@ def __init__(self, corpus, df_dump, p_query=['lemma'], mws=10,
             return
 
         # init corpus
-        self.corpus = corpus.copy()
-
-        # # what's in the dump?
-        # self.df_dump = df_dump
-        # self.size = len(df_dump)
+        self.corpus = corpus
 
         # maximum window size (= context)
         self.mws = mws
 
         # determine layer to work on
         self.p_query = [p_query] if isinstance(p_query, str) else p_query
-        available_attributes = self.corpus.attributes_available['attribute'].values
+        available_attributes = self.corpus.available_attributes()['attribute'].values
         if not set(self.p_query).issubset(set(available_attributes)):
-            logger.warning('specfied p-attribute(s) ("%s") not available' % " ".join(self.p_query))
+            logger.warning('specified p-attribute(s) ("%s") not available' % " ".join(self.p_query))
             logger.warning('falling back to primary layer')
             self.p_query = ['word']
 
-        # collect cpos of matches and context
+        # get df_cooc, f1_set, node_freq
         if df_dump is not None:
-            logger.info('collecting cpos of matches and context')
-            self.df_cooc, self.f1_set = dump2cooc(df_dump, self.mws)
-            self.node_freq = self.corpus.counts.cpos(self.f1_set, self.p_query)
-            logger.info(f'collected {len(self.df_cooc)} corpus positions')
-        else:
-            if df_cooc is None or f1_set is None or node_freq is None:
-                logger.error('if no dump is given, you have to provide all frequencies')
-                return
-            self.df_cooc = df_cooc
-            self.f1_set = f1_set
-            self.node_freq = node_freq
+
+            # get from cache if possible
+            identifier = generate_idx(
+                [df_dump.reset_index()[['match', 'matchend', 'context', 'contextend']], mws, p_query]
+            )
+            f1_set = self.corpus.cache.get(identifier + "-f1_set")
+            df_cooc = self.corpus.cache.get(identifier + "-df_cooc")
+            node_freq = self.corpus.cache.get(identifier + "-node_freq")
+
+            # create and cache otherwise
+            if not isinstance(df_cooc, DataFrame):
+                # create collocation database
+                logger.info('creating')
+                logger.info('.. collecting cpos of matches and context')
+                df_cooc, f1_set = dump2cooc(df_dump, self.mws)
+                node_freq = self.corpus.counts.cpos(f1_set, self.p_query)
+                logger.info(f'.. collected {len(df_cooc)} corpus positions')
+                self.corpus.cache.set(identifier + "-f1_set", f1_set)
+                self.corpus.cache.set(identifier + "-df_cooc", df_cooc)
+                self.corpus.cache.set(identifier + "-node_freq", node_freq)
+
+        elif df_cooc is None or f1_set is None or node_freq is None:
+            logger.error('if no dump is given, you have to provide all frequencies')
+            return
+
+        self.identifier = generate_idx(
+            [df_cooc.head(min(100, len(df_cooc))).reset_index(), f1_set, node_freq]
+        )
+        self.df_cooc = df_cooc
+        self.f1_set = f1_set
+        self.node_freq = node_freq
 
     def count(self, window):
 
@@ -80,57 +97,77 @@ def count(self, window):
         return f
 
     def show(self, window=5, order='log_likelihood', cut_off=100,
-             ams=None, min_freq=2, frequencies=True, flags=None,
-             marginals='corpus', show_negative=False):
+             ams=None, min_freq=2, flags=None, marginals='corpus',
+             show_negative=False):
 
         # consistency check
         if len(self.f1_set) == 0:
             logger.error("nothing to show")
             return DataFrame()
 
-        # get window counts and apply min freq
-        f = self.count(window).rename(columns={'freq': 'f'})
-        vocab = len(f)
-        f1 = f['f'].sum()
-        f = f.loc[f['f'] >= min_freq]
-
-        # get reference frequencies
-        if isinstance(marginals, str):
-            if marginals == 'corpus':
-                N = self.corpus.corpus_size - len(self.f1_set)
-                marginals = self.corpus.marginals(f.index, self.p_query)
+        identifier = generate_idx(
+            [self.identifier, window, ams, min_freq, marginals, flags]
+        )
+
+        collocates = self.corpus.cache.get(identifier)
+
+        if not isinstance(collocates, DataFrame):
+
+            # get window counts and apply min freq
+            logger.info('.. counting')
+            f = self.count(window).rename(columns={'freq': 'f'})
+            vocab = len(f)
+            f1 = f['f'].sum()
+            f = f.loc[f['f'] >= min_freq]
+
+            # get reference frequencies
+            logger.info('.. reference')
+            if isinstance(marginals, str):
+                if marginals == 'corpus':
+                    N = self.corpus.corpus_size - len(self.f1_set)
+                    marginals = self.corpus.marginals(f.index, self.p_query)
+                else:
+                    raise NotImplementedError
+            elif isinstance(marginals, DataFrame):
+                # DataFrame must contain a column 'freq'
+                N = marginals['freq'].sum()
             else:
                 raise NotImplementedError
-        elif isinstance(marginals, DataFrame):
-            # DataFrame must contain a column 'freq'
-            N = marginals['freq'].sum()
-        else:
-            raise NotImplementedError
 
-        # f2 = marginals - node frequencies
-        f2 = marginals[['freq']].rename(columns={'freq': 'marginal'}).join(
-            self.node_freq[['freq']].rename(columns={'freq': 'in_nodes'})
-        )
-        f2 = f2.fillna(0, downcast='infer')
-        f2['f2'] = f2['marginal'] - f2['in_nodes']
-
-        # create dataframe
-        df = f2[['f2']].join(f[['f']])
-        df['f1'] = f1
-        df['N'] = N
-        df = df.fillna(0, downcast='infer')
-
-        # score
-        collocates = score_counts(df, order=order, cut_off=cut_off,
-                                  flags=flags, ams=ams, vocab=vocab)
-
-        if frequencies:
-            # throw away anti-collocates by default
-            if not show_negative:
-                collocates = collocates.loc[collocates['O11'] >= collocates['E11']]
+            # f2 = marginals - node frequencies
+            logger.info('.. calculating')
+            f2 = marginals[['freq']].rename(columns={'freq': 'marginal'}).join(
+                self.node_freq[['freq']].rename(columns={'freq': 'in_nodes'})
+            )
+            f2 = f2.fillna(0, downcast='infer')
+            f2['f2'] = f2['marginal'] - f2['in_nodes']
+
+            # create dataframe
+            df = f[['f']].join(f2[['f2']], how='left')
+            df['f1'] = f1
+            df['N'] = N
+            df = df.fillna(0, downcast='infer')
+
+            # score
+            collocates = score_counts(df, order=order, cut_off=None,
+                                      flags=flags, ams=ams, vocab=vocab)
+
             # add node and marginal frequencies
             collocates = collocates.join(f2[['in_nodes', 'marginal']], how='left')
 
+            # put in cache
+            self.corpus.cache.set(identifier, collocates)
+
+        # throw away anti-collocates by default
+        if not show_negative:
+            collocates = collocates.loc[collocates['O11'] >= collocates['E11']]
+
+        # sort
+        collocates = collocates.sort_values(by=[order, 'item'], ascending=[False, True])
+
+        # apply cut-off
+        collocates = collocates.head(cut_off) if cut_off is not None else collocates
+
         return collocates
 
 
diff --git a/ccc/counts.py b/ccc/counts.py
index 3e2ccf6..5c2b766 100644
--- a/ccc/counts.py
+++ b/ccc/counts.py
@@ -99,13 +99,13 @@ def read_freq_list(path, min_freq=2, columns=None):
     return df, R
 
 
-def cwb_lexdecode(corpus_name, registry_path,
+def cwb_lexdecode(corpus_name, registry_dir,
                   p_att='word', cmd='cwb-lexdecode', min_freq=2):
     """Run cwb-lexdecode: create frequency list of p-attribute.
     CLI: cwb-lexdecode -f -s -P lemma CORPUS_NAME
 
     :param str corpus_name:
-    :param str registry_path:
+    :param str registry_dir:
     :param str p_att:
     :param str cmd: path to binary
     :param int min_freq:
@@ -116,7 +116,7 @@ def cwb_lexdecode(corpus_name, registry_path,
     """
 
     logger.info("running cwb-lexdecode ...")
-    command = [cmd, '-f', '-P', p_att, '-r', registry_path, corpus_name]
+    command = [cmd, '-f', '-P', p_att, '-r', registry_dir, corpus_name]
     lexdecode = subprocess.Popen(
         command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
     )
@@ -126,7 +126,7 @@ def cwb_lexdecode(corpus_name, registry_path,
     return df_counts, R
 
 
-def cwb_scan_corpus(corpus_name, registry_path, path=None,
+def cwb_scan_corpus(corpus_name, registry_dir, path=None,
                     p_atts=['word'], cmd='cwb-scan-corpus', min_freq=2):
     """Run cwb-scan-corpus: create frequency list of p-attribute(s) at corpus positions.
     CLI: cwb-scan-corpus [-R path] CORPUS_NAME p_atts
@@ -134,7 +134,7 @@ def cwb_scan_corpus(corpus_name, registry_path, path=None,
     :return: counts of the p-attribute values (or their combinations)
              at the given positions (if any)
     :param str corpus_name:
-    :param str registry_path:
+    :param str registry_dir:
     :param str p_att:
     :param str cmd: path to binary
     :param int min_freq:
@@ -145,7 +145,7 @@ def cwb_scan_corpus(corpus_name, registry_path, path=None,
     """
 
     logger.info("running cwb-scan-corpus ...")
-    command = [cmd, '-r', registry_path]
+    command = [cmd, '-r', registry_dir]
     if path is not None:
         command += ['-R', path]
     command += [corpus_name] + p_atts
@@ -171,13 +171,13 @@ class Counts:
 
     """
     def __init__(self, corpus_name,
-                 registry_path='/usr/local/share/cwb/registry/'):
+                 registry_dir='/usr/local/share/cwb/registry/'):
 
         self.corpus_name = corpus_name
-        self.registry_path = registry_path
+        self.registry_dir = registry_dir
         self.attributes = Crps(
             self.corpus_name,
-            registry_dir=registry_path
+            registry_dir=registry_dir
         )
 
     def _cpos2patts(self, cpos, p_atts=['word'], ignore=True):
@@ -273,7 +273,7 @@ def dump(self, df_dump, start='match', end='matchend',
                 logger.info("... writing dump temporarily to disk")
                 df_dump[[start, end]].to_csv(f.name, sep="\t", header=None, index=False)
                 df_counts, R = cwb_scan_corpus(
-                    self.corpus_name, self.registry_path, f.name, p_atts
+                    self.corpus_name, self.registry_dir, f.name, p_atts
                 )
 
         df_counts = df_counts.sort_values(by=['freq', 'item'], ascending=False)
@@ -385,7 +385,7 @@ def matches(self, cqp, name, p_atts=["word"], split=False, flags=None, strategy=
                 logger.info("... writing dump temporarily to disk")
                 cqp.Exec(f'dump {name} > "{f.name}";')
                 df_counts, R = cwb_scan_corpus(
-                    self.corpus_name, self.registry_path, f.name, p_atts
+                    self.corpus_name, self.registry_dir, f.name, p_atts
                 )
 
         return df_counts
diff --git a/ccc/cqp.py b/ccc/cqp.py
index 6dfbf3f..e4343ed 100755
--- a/ccc/cqp.py
+++ b/ccc/cqp.py
@@ -5,7 +5,7 @@
 access to CQP
 
 original version by Joerg Asmussen (2008)
-current version by Philipp Heinrich (2022)
+current version by Philipp Heinrich (2023)
 
 """
 import logging
@@ -17,6 +17,7 @@
 import sys
 import threading
 import time
+from glob import glob
 from io import StringIO
 from tempfile import NamedTemporaryFile
 
@@ -94,7 +95,7 @@ def __init__(self, binary="cqp", options='-c', print_version=False):
 
         # init progress controller
         progressthread = threading.Thread(target=self._progressController, args=())
-        progressthread.setDaemon(True)
+        progressthread.daemon = True
         progressthread.start()
 
         # "cqp -c" should print version on startup:
@@ -450,3 +451,63 @@ def nqr_save(self, corpus_name, name='Last'):
         self.Exec(f"save {name};")
         if not self.Ok():
             logger.error('invalid corpus or NQR')
+
+
+def start_cqp(cqp_bin, registry_dir,
+              data_dir=None, corpus_name=None,
+              lib_dir=None, subcorpus_name=None):
+    """Start CQP process, activate (sub-)corpus, set directory paths, and
+    read library (macros and wordlists).
+
+    :param str cqp_bin: /path/to/cqp-binary
+    :param str registry_dir: /path/to/cwb/registry/
+    :param str data_dir: /path/to/data/and/cache/
+    :param str corpus_name: name of corpus in CWB registry
+    :param str lib_dir: /path/to/macros/and/wordlists/
+    :param str subcorpus_name: name of subcorpus (NQR)
+
+    :return: CQP process
+    :rtype: CQP
+
+    """
+
+    cqp = CQP(
+        binary=cqp_bin,
+        options='-c -r ' + registry_dir
+    )
+
+    if data_dir is not None:
+        cqp.Exec(f'set DataDirectory "{data_dir}"')
+
+    if lib_dir is not None:
+
+        # wordlists
+        wordlists = glob(os.path.join(lib_dir, 'wordlists', '*.txt'))
+        for wordlist in wordlists:
+            name = wordlist.split('/')[-1].split('.')[0]
+            abs_path = os.path.abspath(wordlist)
+            cqp_exec = f'define ${name} < "{abs_path}";'
+            cqp.Exec(cqp_exec)
+
+        # macros
+        macros = glob(os.path.join(lib_dir, 'macros', '*.txt'))
+        for macro in macros:
+            abs_path = os.path.abspath(macro)
+            cqp_exec = f'define macro < "{abs_path}";'
+            cqp.Exec(cqp_exec)
+        # for wordlists defined in macros, it is necessary to execute the macro once
+        macros = cqp.Exec("show macro;").split("\n")
+        for macro in macros:
+            # NB: this yields !cqp.Ok() if macro is not zero-valent
+            cqp.Exec(macro.split("(")[0] + "();")
+
+    # initialize corpus after macro definition, so execution of macro doesn't spend time
+    if corpus_name is not None:
+        cqp.Exec(corpus_name)
+    if subcorpus_name is not None:
+        cqp.Exec(subcorpus_name)
+
+    if not cqp.Ok():
+        raise NotImplementedError()
+
+    return cqp
diff --git a/ccc/cqpy.py b/ccc/cqpy.py
index 3a8bd7b..cc817f3 100644
--- a/ccc/cqpy.py
+++ b/ccc/cqpy.py
@@ -50,11 +50,11 @@ def cqpy_load_json(query_path):
     # corpus
     if 'corpus_name' not in query.keys():
         query['corpus_name'] = "BREXIT_V20190522_DEDUP"
-    if 'lib_path' not in query.keys():
-        query['lib_path'] = None
+    if 'lib_dir' not in query.keys():
+        query['lib_dir'] = None
     out['corpus'] = {
         'corpus_name': query['corpus_name'],
-        'lib_path': query['lib_path']
+        'lib_dir': query['lib_dir']
     }
 
     # display
diff --git a/ccc/cwb.py b/ccc/cwb.py
index 636de1e..2076917 100644
--- a/ccc/cwb.py
+++ b/ccc/cwb.py
@@ -2,12 +2,11 @@
 # -*- coding: utf-8 -*-
 """cwb.py
 
-definition of the Corpus and Corpora classes
+definition of the Corpora, Corpus and SubCorpus classes
 
 """
 import logging
 import os
-from glob import glob
 from io import StringIO
 
 # requirements
@@ -18,103 +17,34 @@
 # part of module
 from .cache import Cache, generate_idx, generate_library_idx
 from .cl import Corpus as Attributes
+from .collocates import Collocates
 from .concordances import Concordance
 from .counts import Counts, cwb_scan_corpus
-from .cqp import CQP
-from .dumps import Dump
-from .utils import (chunk_anchors, correct_anchors, dump_left_join,
-                    format_roles, group_lines, preprocess_query, aggregate_matches)
+from .cqp import start_cqp
+from .keywords import Keywords
+from .utils import (aggregate_matches, chunk_anchors, correct_anchors, decode,
+                    dump_left_join, fold_df, format_roles, group_lines,
+                    intersect_intervals, merge_intervals, preprocess_query)
 from .version import __version__
 
 logger = logging.getLogger(__name__)
 
 
-def decode(text):
-    """savely decode a string catching common errors
-
-    """
-    try:
-        text = text.decode('utf-8')
-    except (UnicodeDecodeError, AttributeError):
-        text = ""
-    return text
-
-
-def start_cqp(cqp_bin, registry_path,
-              data_path=None, corpus_name=None,
-              lib_path=None, subcorpus=None):
-    """Start CQP process, activate (sub-)corpus, set paths, and read
-    library (macros and wordlists).
-
-    :param str cqp_bin: /path/to/cqp-binary
-    :param str registry_path: /path/to/cwb/registry/
-    :param str data_path: /path/to/data/and/cache/
-    :param str corpus_name: name of corpus in CWB registry
-    :param str lib_path: /path/to/macros/and/wordlists/
-    :param str subcorpus: name of subcorpus (NQR)
-
-    :return: CQP process
-    :rtype: CQP
-
-    """
-
-    cqp = CQP(
-        binary=cqp_bin,
-        options='-c -r ' + registry_path
-    )
-
-    if data_path is not None:
-        cqp.Exec(f'set DataDirectory "{data_path}"')
-
-    if lib_path is not None:
-
-        # wordlists
-        wordlists = glob(os.path.join(lib_path, 'wordlists', '*.txt'))
-        for wordlist in wordlists:
-            name = wordlist.split('/')[-1].split('.')[0]
-            abs_path = os.path.abspath(wordlist)
-            cqp_exec = f'define ${name} < "{abs_path}";'
-            cqp.Exec(cqp_exec)
-
-        # macros
-        macros = glob(os.path.join(lib_path, 'macros', '*.txt'))
-        for macro in macros:
-            abs_path = os.path.abspath(macro)
-            cqp_exec = f'define macro < "{abs_path}";'
-            cqp.Exec(cqp_exec)
-        # for wordlists defined in macros, it is necessary to execute the macro once
-        macros = cqp.Exec("show macro;").split("\n")
-        for macro in macros:
-            # NB: this yields !cqp.Ok() if macro is not zero-valent
-            cqp.Exec(macro.split("(")[0] + "();")
-
-    # initialize corpus after macro definition, so execution of macro doesn't spend time
-    if corpus_name is not None:
-        cqp.Exec(corpus_name)
-    if subcorpus is not None:
-        cqp.Exec(subcorpus)
-
-    if not cqp.Ok():
-        raise NotImplementedError()
-
-    return cqp
-
-
 class Corpora:
     """Interface to CWB-indexed corpora.
 
     """
 
     def __init__(self, cqp_bin='cqp',
-                 registry_path='/usr/local/share/cwb/registry/'):
+                 registry_dir='/usr/local/share/cwb/registry/'):
         """Establish connection to registry and CQP.
 
         :param str cqp_bin: /path/to/cqp-binary
-        :param str registry_path: /path/to/cwb/registry/
+        :param str registry_dir: /path/to/cwb/registry/
 
         """
 
-        self.registry_path = registry_path
+        self.registry_dir = registry_dir
         self.cqp_bin = cqp_bin
 
     def __str__(self):
@@ -128,8 +58,8 @@ def __str__(self):
         corpora = self.show()
 
         return '\n' + '\n'.join([
-            f'registry path: "{self.registry_path}"',
-            f'cqp binary   : "{self.cqp_bin}"',
+            f'registry directory: "{self.registry_dir}"',
+            f'cqp binary:         "{self.cqp_bin}"',
             f'found {len(corpora)} corpora:',
             corpora.to_string(),
         ])
@@ -150,7 +80,7 @@ def show(self):
         """
 
         # get all corpora defined in registry
-        cqp = start_cqp(self.cqp_bin, self.registry_path)
+        cqp = start_cqp(self.cqp_bin, self.registry_dir)
         corpora = cqp.Exec("show corpora;").split("\n")
         cqp.__del__()
 
@@ -160,7 +90,7 @@ def show(self):
         for corpus_name in corpora:
             try:
                 sizes.append(len(Attributes(
-                    corpus_name, registry_dir=self.registry_path
+                    corpus_name, registry_dir=self.registry_dir
                 ).attribute('word', 'p')))
                 names.append(corpus_name)
             except SystemError:
@@ -173,13 +103,13 @@ def show(self):
 
         return corpora
 
-    def activate(self, corpus_name,
-                 lib_path=None, data_path=None):
+    def corpus(self, corpus_name,
+               lib_dir=None, data_dir=None):
         """Activate a corpus.
 
         :param str corpus_name: name of corpus in CWB registry
-        :param str lib_path: /path/to/macros/and/wordlists/
-        :param str data_path: /path/to/data/and/cache/
+        :param str lib_dir: /path/to/macros/and/wordlists/
+        :param str data_dir: /path/to/data/and/cache/
 
         :return: corpus
         :rtype: Corpus
@@ -187,40 +117,40 @@ def activate(self, corpus_name,
         """
 
         return Corpus(corpus_name,
-                      lib_path=lib_path,
+                      lib_dir=lib_dir,
                       cqp_bin=self.cqp_bin,
-                      registry_path=self.registry_path,
-                      data_path=data_path)
+                      registry_dir=self.registry_dir,
+                      data_dir=data_dir)
 
 
-def init_data_path(data_path, corpus_name, lib_path=None):
+def init_data_dir(data_dir, corpus_name, lib_dir=None):
     """ get a data directory / ensure that the given one complies with schema
 
     """
 
-    if data_path is None:
-        data_path = os.path.join("/tmp", "ccc-" + str(__version__))
-    if not isinstance(data_path, str):
-        raise ValueError("parameter data_path must be str")
+    if data_dir is None:
+        data_dir = os.path.join("/tmp", "ccc-" + str(__version__))
+    if not isinstance(data_dir, str):
+        raise ValueError("parameter data_dir must be str")
 
     # generate library idx to invalidate cache when updated
-    if lib_path is not None:
-        lib_idx = generate_library_idx(lib_path, 'lib-', 7)
+    if lib_dir is not None:
+        lib_idx = generate_library_idx(lib_dir, 'lib-', 7)
     else:
         lib_idx = "lib-vanilla"
 
     # each corpus has its separate directory for each library
     subdir = corpus_name + "-" + lib_idx
-    if not data_path.endswith(subdir):
-        data_path = os.path.join(data_path, subdir)
+    if not data_dir.endswith(subdir):
+        data_dir = os.path.join(data_dir, subdir)
 
     # create directory
     try:
-        os.makedirs(data_path, exist_ok=True)
+        os.makedirs(data_dir, exist_ok=True)
     except PermissionError:
-        logger.error(f'no write permission at "{data_path}"')
+        logger.error(f'no write permission at "{data_dir}"')
     else:
-        return data_path
+        return data_dir
 
 
 class Corpus:
@@ -228,55 +158,52 @@ class Corpus:
 
     """
 
-    def __init__(self, corpus_name, lib_path=None, cqp_bin='cqp',
-                 registry_path='/usr/local/share/cwb/registry/',
-                 data_path=None):
+    def __init__(self, corpus_name, lib_dir=None, cqp_bin='cqp',
+                 registry_dir='/usr/local/share/cwb/registry/',
+                 data_dir=None):
         """Establish connection to CQP and corpus attributes, set paths, read
         library.
 
         Raises KeyError if corpus not in registry.
 
         data directory contains subdirectories for each corpus and
-        each library (/<data-path>/<corpus>-<lib-idx>/CACHE)
+        each library (/<data-dir>/<corpus>-<lib-idx>/CACHE)
         - marginals_complex
         - dump_from_s_att
         - dump_from_query
 
         :param str corpus_name: name of corpus in CWB registry
-        :param str lib_path: /path/to/macros/and/wordlists/
+        :param str lib_dir: /path/to/macros/and/wordlists/
         :param str cqp_bin: /path/to/cqp-binary
-        :param str registry_path: /path/to/cwb/registry/
-        :param str data_path: /path/to/data/and/cache/
+        :param str registry_dir: /path/to/cwb/registry/
+        :param str data_dir: /path/to/data/and/cache/
 
         """
 
-        # preprocess data path
-        data_path = init_data_path(data_path, corpus_name, lib_path)
+        # preprocess data dir
+        data_dir = init_data_dir(data_dir, corpus_name, lib_dir)
 
         # save paths
-        self.data_path = data_path
-        self.registry_path = registry_path
+        self.data_dir = data_dir
+        self.registry_dir = registry_dir
         self.cqp_bin = cqp_bin
-        self.lib_path = lib_path
+        self.lib_dir = lib_dir
 
         # init (sub-)corpus information
         self.corpus_name = corpus_name
-        self.subcorpus = None
+        self.subcorpus_name = None
 
         # init attributes
-        self.attributes = Attributes(self.corpus_name, registry_dir=self.registry_path)
+        self.attributes = Attributes(self.corpus_name, registry_dir=self.registry_dir)
 
         # get corpus size
         self.corpus_size = len(self.attributes.attribute('word', 'p'))
 
-        # get available corpus attributes
-        self.attributes_available = self._attributes_available()
-
         # init cache
-        self.cache = Cache(os.path.join(self.data_path, "CACHE"))
+        self.cache = Cache(os.path.join(self.data_dir, "CACHE"))
 
         # init counts
-        self.counts = Counts(self.corpus_name, self.registry_path)
+        self.counts = Counts(self.corpus_name, self.registry_dir)
 
     def __str__(self):
         """Method for printing.
@@ -287,12 +214,8 @@ def __str__(self):
         """
 
         return '\n' + '\n'.join([
-            f'ccc.Corpus: "{self.corpus_name}"',
-            f'size      : {self.corpus_size}',
-            f'data      : {self.data_path}',
-            f'subcorpus : {self.subcorpus}',
-            '\navailable positional and structural attributes:',
-            self.attributes_available.to_string(),
+            f'corpus: {self.corpus_name} ({self.corpus_size} tokens)',
+            f'data:   {self.data_dir}',
         ])
 
     def __repr__(self):
@@ -301,7 +224,7 @@ def __repr__(self):
         """
         return self.__str__()
 
-    def _macros_available(self):
+    def available_macros(self):
         """Get available macros (either system-defined or via library).
 
         :return: defined macros
@@ -315,7 +238,7 @@ def _macros_available(self):
 
         return defined_macros
 
-    def _wordlists_available(self):
+    def available_wordlists(self):
         """Get available wordlists.
 
         :return: defined wordlists
@@ -333,7 +256,7 @@ def _wordlists_available(self):
 
         return names
 
-    def _attributes_available(self):
+    def available_attributes(self):
         """Get indexed p- and s-attributes. Will be run once when initializing
         the corpus.
 
@@ -369,11 +292,11 @@ def start_cqp(self):
 
         return start_cqp(
             self.cqp_bin,
-            self.registry_path,
-            self.data_path,
+            self.registry_dir,
+            self.data_dir,
             self.corpus_name,
-            self.lib_path,
-            self.subcorpus
+            self.lib_dir,
+            self.subcorpus_name
         )
 
     def copy(self):
@@ -386,17 +309,17 @@ def copy(self):
 
         return Corpus(
             self.corpus_name,
-            self.lib_path,
+            self.lib_dir,
             self.cqp_bin,
-            self.registry_path,
-            self.data_path
+            self.registry_dir,
+            self.data_dir
         )
 
     ################
     # p-attributes #
     ################
     def cpos2patts(self, cpos, p_atts=['word'], ignore=True):
-        """Retrieve p-attributes of corpus position.
+        """Retrieve p-attributes at corpus position.
 
         :param int cpos: corpus position to fill
         :param list p_atts: p-attribute(s) to fill position with
@@ -410,18 +333,30 @@ def cpos2patts(self, cpos, p_atts=['word'], ignore=True):
         return self.counts._cpos2patts(cpos, p_atts, ignore)
 
     def marginals(self, items=None, p_atts=['word'], flags=0, pattern=False):
+        """Extract marginal frequencies. If no items are given, return
+        frequencies of all items.
+
+        :param list items: items to get marginals for
+        :param str p_att: p-attribute to get frequencies for
+        :param int flags: 1 = %c, 2 = %d, 3 = %cd (will activate wildcards)
+        :param bool pattern: activate wildcards?
+
+        :return: frequencies of the items in the whole corpus indexed by items
+        :rtype: FreqFrame
+
+        """
 
         # allow lazy evocation
         p_atts = [p_atts] if isinstance(p_atts, str) else p_atts
 
         # simple
         if len(p_atts) == 1 and items is not None:
-            return self.marginals_simple(items, p_atts[0], flags, pattern)
+            return self._marginals_simple(items, p_atts[0], flags, pattern)
 
         else:
-            return self.marginals_complex(items, p_atts)
+            return self._marginals_complex(items, p_atts)
 
-    def marginals_simple(self, items, p_att='word', flags=0, pattern=False):
+    def _marginals_simple(self, items, p_att='word', flags=0, pattern=False):
         """Extract marginal frequencies for given unigrams or unigram patterns
         of a single p-attribute.  0 if not in corpus.  For
         combinations of p-attributes, see marginals_complex.
@@ -436,6 +371,9 @@ def marginals_simple(self, items, p_att='word', flags=0, pattern=False):
 
         """
 
+        if self.subcorpus_name is not None:
+            logger.warning('retrieving corpus marginals, not subcorpus marginals')
+
         pattern = True if flags > 0 else pattern
         att = self.attributes.attribute(p_att, 'p')
 
@@ -462,7 +400,7 @@ def att_frequency(att, item):
 
         return df
 
-    def marginals_complex(self, items, p_atts=['word']):
+    def _marginals_complex(self, items, p_atts=['word']):
         """Extract marginal frequencies for p-attribute combinations,
         e.g. ["lemma", "pos"].  0 if not in corpus.  Marginals are
         retrieved using cwb-scan-corpus, result is cached.
@@ -475,8 +413,11 @@ def marginals_complex(self, items, p_atts=['word']):
 
         """
 
+        if self.subcorpus_name is not None:
+            logger.warning('retrieving corpus marginals, not subcorpus marginals')
+
         # get all marginals for p-att combination
-        identifier = "_".join(p_atts) + "_marginals"
+        identifier = "-".join(p_atts) + "-marginals"
         df = self.cache.get(identifier)
         if df is not None:
             # get from cache if possible
@@ -484,20 +425,19 @@ def marginals_complex(self, items, p_atts=['word']):
         else:
             # calculate all marginals for p-att combination
             df, R = cwb_scan_corpus(
-                self.corpus_name, self.registry_path, p_atts=p_atts, min_freq=0
+                self.corpus_name, self.registry_dir, p_atts=p_atts, min_freq=0
             )
             self.cache.set(identifier, df)
 
         if items is not None:
             # preprocess tuples
-            items = [" ".join(i) for i in items] if isinstance(items[0], tuple) \
-                else items
+            items = [" ".join(i) for i in items] if isinstance(items[0], tuple) else items
             # select relevant rows
             df = df.reindex(items)
             df = df.fillna(0, downcast='infer')
 
         # sort by frequency
-        df.sort_values(by='freq')
+        df = df.sort_values(by=['freq', 'item'], ascending=False)
 
         return df
 
@@ -532,6 +472,7 @@ def show_nqr(self):
         """
         cqp = self.start_cqp()
         cqp_return = cqp.Exec("show named;")
+
         try:
             df = read_csv(StringIO(cqp_return), sep="\t", header=None)
             df.columns = ["storage", "corpus:subcorpus", "size"]
@@ -542,47 +483,11 @@ def show_nqr(self):
             df = df[['corpus', 'subcorpus', 'size', 'storage']]
         except EmptyDataError:
             logger.info("no subcorpora defined")
-            df = DataFrame()
+            df = DataFrame(columns=['corpus', 'subcorpus', 'size', 'storage'])
 
         cqp.__del__()
-        return df
-
-    def activate_subcorpus(self, nqr=None, df_dump=None):
-        """Activate a Named Query Result (NQR).
-
-        - If no df_dump is given, this sets self.subcorpus and logs an
-          error if NQR is not defined.
-        - If a df_dump is given, the df_dump will be undumped and named
-          NQR.
-
-        :param str nqr: NQR defined in CQP
-        :param DataFrame df_dump: DataFrame indexed by (match, matchend)
-                                  with optional columns 'target' and 'keyword'
-
-        """
-
-        subcorpus = self.copy()
-
-        if nqr is not None:
 
-            # create NQR
-            if df_dump is not None:
-                cqp = self.start_cqp()
-                cqp.nqr_from_dump(df_dump, nqr)
-                cqp.nqr_save(self.corpus_name, nqr)
-                cqp.__del__()
-            if nqr not in self.show_nqr()['subcorpus'].values:
-                logger.error(f'subcorpus "{nqr}" not defined)')
-            else:
-                logger.info(f'switched to subcorpus "{nqr}"')
-
-        else:
-            logger.info(f'switched to corpus "{self.corpus_name}"')
-
-        # activate subcorpus
-        subcorpus.subcorpus = nqr
-
-        return subcorpus
+        return df
 
     ##################
     # CREATING DUMPS #
@@ -613,7 +518,7 @@ def dump_from_s_att(self, s_att, annotation=True):
         """
 
         # retrieve from cache if possible
-        identifier = s_att + "_spans"
+        identifier = s_att + "-spans"
         df = self.cache.get(identifier)
         if df is not None:
             logger.info(f'using cached version of spans of "{s_att}"')
@@ -675,15 +580,15 @@ def dump_from_query(self, query, s_query=None, anchors=[],
         """
 
         # identify query
-        if self.subcorpus is not None:
+        if self.subcorpus_name is not None:
             # check subcorpus size to avoid confusion when re-naming
             cqp = self.start_cqp()
-            sbcrpssize = cqp.Exec(f"size {self.subcorpus}")
+            sbcrpssize = cqp.Exec(f"size {self.subcorpus_name}")
             cqp.__del__()
         else:
             sbcrpssize = None
         identifier = generate_idx([
-             query, s_query, anchors, match_strategy, self.subcorpus, sbcrpssize
+             query, s_query, anchors, match_strategy, self.subcorpus_name, sbcrpssize
         ], prefix="df_dump:")
 
         # retrieve from cache if possible
@@ -1055,16 +960,20 @@ def query_s_att(self, s_att, values=set(), name=None):
                 logger.info(f"restricting spans using {len(values)} values")
                 df_spans = df_spans.loc[df_spans[s_att].isin(values)]
 
-        # save as NQR
-        if name is not None:
-            # undump the dump and save to disk
-            cqp = self.start_cqp()
-            cqp.nqr_from_dump(df_spans, name)
-            cqp.nqr_save(self.corpus_name, name)
-            cqp.__del__()
+        # if this is a subcorpus, only return intersection
+        if self.subcorpus_name is not None:
+            columns_left = list(self.df.columns)
+            columns_right = list(df_spans.columns)
+            intersection = intersect_intervals(
+                self.df.reset_index()[['match', 'matchend'] + columns_left].values.tolist(),
+                df_spans.reset_index()[['match', 'matchend'] + columns_right].values.tolist()
+            )
+            df_spans = DataFrame(intersection)
+            df_spans.columns = ['match', 'matchend'] + columns_left + columns_right
+            df_spans = df_spans.set_index(['match', 'matchend'])
 
-        # return proper Dump
-        return Dump(self.copy(), df_spans, name_cqp=name)
+        # return SubCorpus
+        return self.subcorpus(subcorpus_name=name, df_dump=df_spans)
 
     def query_cqp(self, cqp_query, context=20, context_left=None,
                   context_right=None, context_break=None, corrections=dict(),
@@ -1115,14 +1024,6 @@ def query_cqp(self, cqp_query, context=20, context_left=None,
         if propagate_error and isinstance(df_dump, str):
             return df_dump
 
-        # if dump has been retrieved from cache, NQR might not exist
-        if save and (self.show_nqr().empty or name not in self.show_nqr()['subcorpus'].values):
-            # undump the dump and save to disk
-            cqp = self.start_cqp()
-            cqp.nqr_from_dump(df_dump, name)
-            cqp.nqr_save(self.corpus_name, name)
-            cqp.__del__()
-
         # empty return?
         if len(df_dump) == 0:
             logger.warning("found 0 matches")
@@ -1137,12 +1038,8 @@ def query_cqp(self, cqp_query, context=20, context_left=None,
             # apply corrections to anchor points
             df_dump = correct_anchors(df_dump, corrections)
 
-        # return proper dump
-        return Dump(
-            self.copy(),
-            df_dump,
-            name_cqp=name
-        )
+        # return SubCorpus
+        return self.subcorpus(subcorpus_name=name, df_dump=df_dump)
 
     def query(self, cqp_query=None, context=20, context_left=None,
               context_right=None, context_break=None,
@@ -1189,7 +1086,7 @@ def quick_query(self, s_context, topic_query="", filter_queries=[], match_strate
         """
 
         if len(topic_query) == 0:
-            identifier = generate_idx([self.subcorpus, filter_queries, s_context, match_strategy], prefix='Query')
+            identifier = generate_idx([self.subcorpus_name, filter_queries, s_context, match_strategy], prefix='Query')
 
             cqp = self.start_cqp()
             cqp.Exec(f'set MatchingStrategy "{match_strategy}";')
@@ -1207,8 +1104,8 @@ def quick_query(self, s_context, topic_query="", filter_queries=[], match_strate
             return identifier
 
         # IDENTIFY
-        topic_identifier = generate_idx([self.subcorpus, topic_query, s_context, match_strategy], prefix='Query')
-        filter_identifier = generate_idx([self.subcorpus, topic_query, s_context, match_strategy, filter_queries], prefix='Query')
+        topic_identifier = generate_idx([self.subcorpus_name, topic_query, s_context, match_strategy], prefix='Query')
+        filter_identifier = generate_idx([self.subcorpus_name, topic_query, s_context, match_strategy, filter_queries], prefix='Query')
 
         # CHECK CQP
         cqp = self.start_cqp()
@@ -1254,6 +1151,18 @@ def quick_conc(self, topic_query, s_context, window, order=42,
         :rtype: list(dict)
         """
 
+        logger.info('quick concordancing should be quick')
+        logger.info('.. START')
+
+        if order == 'first':
+            cqp_order = ""
+
+        elif isinstance(order, int):
+            cqp_order = f'randomize {order}'
+
+        else:
+            raise ValueError
+
         if len(topic_query) == 0:
 
             queries = {**highlight_queries, **filter_queries}
@@ -1263,11 +1172,11 @@ def quick_conc(self, topic_query, s_context, window, order=42,
             cqp = self.start_cqp()
 
             # init CONTEXT (TextConstellation)
+            cqp.Exec(f'sort {identifier} {cqp_order};')
             cqp.Exec(f'cut {identifier} {cut_off};')
             df_context = cqp.Dump(f'{identifier};')
-            dump_context = Dump(self.copy(), df_context, None)
-            dump_context = dump_context.set_context(context_break=s_context)
-            df_context = dump_context.df[['contextid']]
+            subcorpus_context = self.subcorpus(None, df_context).set_context(context_break=s_context)
+            df_context = subcorpus_context.df[['contextid']]
             df_context = df_context.reset_index().set_index('contextid')
 
             # HIGHLIGHT
@@ -1276,9 +1185,8 @@ def quick_conc(self, topic_query, s_context, window, order=42,
                 cqp.Exec(f'Temp = {query};')
                 df_query = cqp.Dump('Temp;')
                 if len(df_query) > 0:
-                    dump_query = Dump(self.copy(), df_query, None)
-                    dump_query = dump_query.set_context(context_break=s_context)
-                    df_query = dump_query.df[['contextid']]
+                    subcorpus_query = self.subcorpus(None, df_query).set_context(context_break=s_context)
+                    df_query = subcorpus_query.df[['contextid']]
                     df_agg = aggregate_matches(df_query, name)
                     df_context = df_context.join(df_agg)
                 else:
@@ -1304,49 +1212,335 @@ def quick_conc(self, topic_query, s_context, window, order=42,
         else:
 
             # INIT CQP
+            logger.info('.. INIT')
             identifier = self.quick_query(s_context, topic_query, filter_queries.values(), match_strategy)
             cqp = self.start_cqp()
 
-            # init CONTEXT (TextConstellation)
+            # set CONTEXT (TextConstellation)
+            logger.info('.. SET')
             cqp.Exec(f'{identifier};')
+            if len(filter_queries) == 0:
+                cqp.Exec(f'sort {identifier} {cqp_order};')
+                cqp.Exec(f'cut {identifier} {cut_off};')
             df_context = cqp.Dump(f'{identifier};')
-            dump_context = Dump(self.copy(), df_context, None)
-            dump_context = dump_context.set_context(window, s_context)
-            df_context = dump_context.df[['contextid', 'context', 'contextend']]
+            subcorpus_context = self.subcorpus(None, df_context).set_context(window, s_context)
+            df_context = subcorpus_context.df[['contextid', 'context', 'contextend']]
 
             # index by TOPIC MATCHES
+            logger.info('.. INDEX')
             cqp.Exec(f'Temp = {topic_query};')
             df_query = cqp.Dump('Temp;')
-            dump_query = Dump(self.copy(), df_query, None)
-            dump_query = dump_query.set_context(window, s_context)
-            df_context = dump_left_join(df_context, dump_query.df, 'topic', drop=True, window=window)
+            subcorpus_query = self.subcorpus(None, df_query).set_context(window, s_context)
+            df_context = dump_left_join(df_context, subcorpus_query.df, 'topic', drop=True, window=window)
             df_context = df_context.set_index(['match_topic', 'matchend_topic'])
             df_context.index.names = ['match', 'matchend']
 
-            # FILTER according to window size
+            # filter according to WINDOW size
+            logger.info('.. FILTER')
             for name, query in filter_queries.items():
                 cqp.Exec(f'Temp = {query};')
                 df_query = cqp.Dump('Temp;')
-                dump_query = Dump(self.copy(), df_query, None)
-                dump_query = dump_query.set_context(window, s_context)
-                df_context = dump_left_join(df_context, dump_query.df, name, drop=True, window=window)
+                subcorpus_query = self.subcorpus(None, df_query).set_context(window, s_context)
+                df_context = dump_left_join(df_context, subcorpus_query.df, name, drop=True, window=window)
                 df_context = df_context.drop([c + "_" + name for c in ['match', 'matchend', 'offset']], axis=1)
 
-            # HIGHLIGHT
+            # highlight
+            logger.info('.. HIGHLIGHT')
             for name, query in highlight_queries.items():
                 cqp.Exec(f'Temp = {query};')
                 df_query = cqp.Dump('Temp;')
-                dump_query = Dump(self.copy(), df_query, None)
-                dump_query = dump_query.set_context(window, s_context)
-                df_context = dump_left_join(df_context, dump_query.df, name, drop=False, window=window)
+                subcorpus_query = self.subcorpus(None, df_query).set_context(window, s_context)
+                df_context = dump_left_join(df_context, subcorpus_query.df, name, drop=False, window=window)
 
             cqp.__del__()
 
-            # ACTUAL CONCORDANCING
+            # formatting
+            logger.info('.. FORMAT')
             hkeys = list(highlight_queries.keys())
             df = group_lines(df_context, hkeys)
             conc = Concordance(self.copy(), df)
             lines = conc.lines(form='dict', p_show=p_show, s_show=s_show, order=order, cut_off=cut_off)
             output = lines.apply(lambda row: format_roles(row, hkeys, s_show, window, htmlify_meta=True), axis=1)
 
+        logger.info('.. FINISH')
         return list(output)
+
+    def subcorpus(self, subcorpus_name=None, df_dump=None, overwrite=True):
+
+        return SubCorpus(subcorpus_name, df_dump, self.corpus_name,
+                         self.lib_dir, self.cqp_bin, self.registry_dir, self.data_dir, overwrite)
+
+
+class SubCorpus(Corpus):
+
+    def __init__(self, subcorpus_name, df_dump, corpus_name, lib_dir,
+                 cqp_bin, registry_dir, data_dir, overwrite=True):
+        """
+        :param str subcorpus_name: name of NQR in CQP
+        :param DataFrame df_dump: a "DumpFrame"
+
+        a "DumpFrame" is a dataframe indexed by (match, matchend),
+        i.e. non-overlapping corpus spans represented by integers,
+        and without missing values.
+
+        CREATION
+
+        dump_from_query:
+        - index <int, int> match, matchend: cpos
+        - columns <int> 0*, .., 9*: anchor points (optional; missing = -1)
+
+        dump_from_s_att:
+        - index <int, int> match, matchend: cpos
+        - column <int> $s_cwbid: id
+        - column <str> $s: annotation (optional)
+
+        TRANSFORMATION (additional columns are preserved)
+
+        dump2patt:
+        - index <int, int> match, matchend: cpos
+        - column <str> $p: " "-joined tokens (match..matchend or regions of input columns)
+
+        dump2satt:
+        - index <int, int> match, matchend: cpos
+        - column <int> $s_cwbid: id (missing = -1) of s_att at match cpos
+        - column <int> $s_span: cpos (missing = -1)
+        - column <int> $s_spanend: cpos (missing = -1)
+        - column <str> $s*: annotation (optional)
+
+        dump2context:
+        - index <int, int> match, matchend: cpos
+        - column <int> contextid: of id (optional; duplicate of $s_cwbid)
+        - column <int> context: cpos
+        - column <int> contextend: cpos
+        - column <int> $s_cwbid: id (optional; missing = -1)
+        - column <int> $s_span: cpos (missing = -1)
+        - column <int> $s_spanend: cpos (missing = -1)
+
+        QUERY ALIASES
+
+        query_cqp:
+        - index <int, int> match, matchend: cpos
+        - columns <int> 0*, .., 9*: anchor points (optional; missing = -1)
+        - column <int> contextid: of id (optional)
+        - column <int> context: cpos (optional)
+        - column <int> contextend: cpos (optional)
+
+        query_s_att:
+        - index <int, int> match, matchend: cpos
+        - column <int> $s_cwbid: id
+        - column <str> $s: annotation (optional)
+
+        """
+
+        super().__init__(corpus_name, lib_dir, cqp_bin, registry_dir, data_dir)
+
+        if (subcorpus_name is None and df_dump is None):
+            logger.warning('no subcorpus information provided, returning Corpus')
+
+        else:
+            if subcorpus_name is None:  # (and df_dump is not None)
+                subcorpus_name = generate_idx([df_dump], prefix='df_')
+                self._assign(subcorpus_name, df_dump, overwrite)
+
+            elif df_dump is None:  # (and subcorpus_name is not None)
+                cqp = self.start_cqp()
+                df_dump = cqp.Dump(subcorpus=subcorpus_name)
+                cqp.__del__()
+
+            else:      # (both df_dump and subcorpus_name are given)
+                self._assign(subcorpus_name, df_dump, overwrite)
+
+            self.df = df_dump
+            self.subcorpus_name = subcorpus_name
+
+    def _assign(self, subcorpus_name, df_dump, overwrite):
+
+        if subcorpus_name in self.show_nqr()['subcorpus'].values:
+            # NQR exists
+            if overwrite:
+                logger.info(f'NQR "{subcorpus_name}" exists, overwriting')
+                # create in CQP
+                cqp = self.start_cqp()
+                cqp.nqr_from_dump(df_dump, subcorpus_name)
+                cqp.nqr_save(self.corpus_name, subcorpus_name)
+                cqp.__del__()
+            else:
+                logger.info(f'NQR "{subcorpus_name}" already exists')
+
+        else:
+            # create in CQP
+            cqp = self.start_cqp()
+            cqp.nqr_from_dump(df_dump, subcorpus_name)
+            cqp.nqr_save(self.corpus_name, subcorpus_name)
+            cqp.__del__()
+
+        if subcorpus_name not in self.show_nqr()['subcorpus'].values:
+            logger.error(f'could not assigne NQR "{subcorpus_name}" from dataframe')
+        else:
+            logger.info(f'assigned NQR "{subcorpus_name}" from dataframe')
+
+    def __str__(self):
+
+        return '\n' + '\n'.join([
+            f'corpus:    {self.corpus_name} ({self.corpus_size} tokens)',
+            f'data:      {self.data_dir}',
+            f'subcorpus: {self.subcorpus_name} ({len(self.df)} spans)'
+        ]) + '\n'
+
+    def __repr__(self):
+        """Info string.
+
+        """
+        return self.__str__()
+
+    def set_context(self, context=None, context_break=None,
+                    context_left=None, context_right=None):
+        """Set context in the dump.
+
+        """
+        # pre-process context
+        if context_left is None:
+            context_left = context
+        if context_right is None:
+            context_right = context
+
+        # set context
+        df = self.dump2context(
+            self.df, context_left, context_right, context_break
+        )
+
+        return self.subcorpus(self.subcorpus_name, df)
+
+    def correct_anchors(self, corrections):
+        """Correct anchors by integer offsets.
+
+        """
+        self.df = correct_anchors(self.df, corrections)
+
+    def breakdown(self, p_atts=['word'], flags=""):
+        """Frequency breakdown of match..matchend.
+
+        """
+
+        logger.info('creating frequency breakdown')
+        breakdown = self.counts.dump(
+            df_dump=self.df,
+            start='match', end='matchend',
+            p_atts=p_atts, strategy=1
+        )
+
+        breakdown = fold_df(breakdown, flags)
+
+        return breakdown
+
+    def matches(self):
+        """
+        :return: cpos of (match .. matchend) regions
+        :rtype: set
+        """
+
+        identifier = generate_idx([self.df.reset_index()[['match', 'matchend']]]) + "-matches"
+        f1 = self.cache.get(identifier)
+        if not isinstance(f1, set):
+            f1 = set()
+            for match, matchend in self.df.index:
+                f1.update(range(match, matchend + 1))
+            self.cache.set(identifier, f1)
+        return f1
+
+    def context(self):
+        """
+        :return: cpos of (context .. contextend) regions including matches
+        :rtype: DataFrame
+        """
+
+        identifier = generate_idx([self.df[['context', 'contextend']]]) + "-contexts"
+        df = self.cache.get(identifier)
+        if not isinstance(df, DataFrame):
+            df = DataFrame.from_records(merge_intervals(
+                self.df[['context', 'contextend']].values.tolist()
+            ), columns=['context', 'contextend'])
+            self.cache.set(identifier, df)
+        return df
+
+    def marginals(self, items=None, p_atts=['word'], start='match', end='matchend'):
+        """
+        :return: counts of (start .. end) regions including matches
+        :rtype: DataFrame
+        """
+
+        identifier = generate_idx([self.df.reset_index()[[start, end]]]) + "-".join(p_atts) + "-marginals"
+        df = self.cache.get(identifier)
+        if not isinstance(df, DataFrame):
+            df = self.counts.dump(
+                self.df, start=start, end=end, p_atts=p_atts, split=True
+            )
+            self.cache.set(identifier, df)
+
+        if items is not None:
+            # preprocess tuples
+            items = [" ".join(i) for i in items] if isinstance(items[0], tuple) else items
+            # select relevant rows
+            df = df.reindex(items)
+            df = df.fillna(0, downcast='infer')
+
+        # sort by frequency
+        df = df.sort_values(by=['freq', 'item'], ascending=False)
+
+        return df
+
+    def concordance(self, form='simple', p_show=['word'], s_show=[],
+                    order='first', cut_off=100, matches=None,
+                    slots=None, cwb_ids=False):
+
+        conc = Concordance(
+            self.copy(),
+            df_dump=self.df
+        )
+
+        return conc.lines(
+            form=form,
+            p_show=p_show,
+            s_show=s_show,
+            order=order,
+            cut_off=cut_off,
+            matches=matches,
+            slots=slots,
+            cwb_ids=cwb_ids
+        )
+
+    def collocates(self, p_query=['lemma'], mws=20, window=5,
+                   order='O11', cut_off=100, ams=None, min_freq=2,
+                   flags=None, marginals='corpus',
+                   show_negative=False):
+
+        mws = max(mws, window)
+
+        coll = Collocates(self.copy(), self.df, p_query, mws)
+
+        return coll.show(
+            window=window,
+            order=order,
+            cut_off=cut_off,
+            ams=ams,
+            min_freq=min_freq,
+            flags=flags,
+            marginals=marginals,
+            show_negative=show_negative
+        )
+
+    def keywords(self, p_query=['lemma'], order='O11', cut_off=100,
+                 ams=None, min_freq=2, flags=None,
+                 marginals='corpus', show_negative=False):
+
+        kw = Keywords(self.copy(), self.df, p_query)
+
+        return kw.show(
+            order=order,
+            cut_off=cut_off,
+            ams=ams,
+            min_freq=min_freq,
+            flags=flags,
+            marginals=marginals,
+            show_negative=show_negative
+        )
diff --git a/ccc/discoursemes.py b/ccc/discoursemes.py
index d21cec5..cd65c88 100644
--- a/ccc/discoursemes.py
+++ b/ccc/discoursemes.py
@@ -12,11 +12,12 @@
 from pandas import DataFrame, concat
 
 # part of module
-from . import Corpus
+from . import Corpus, SubCorpus
+from .cache import generate_idx
 from .collocates import Collocates, dump2cooc
 from .concordances import Concordance
-from .dumps import Dump
-from .utils import dump_left_join, format_cqp_query, aggregate_matches, group_lines, format_roles
+from .utils import (aggregate_matches, dump_left_join, format_cqp_query,
+                    format_roles, group_lines)
 
 logger = logging.getLogger(__name__)
 
@@ -80,14 +81,128 @@
 # d1 = {(o1, m1, m1e), ...}
 # ...
 
-##########################################################
-# (2) OUTER JOIN ON CONTEXTID: FULL TEXTUAL COOCCURRENCE #
-##########################################################
+#####################################################
+# (2) OUTER JOIN ON CONTEXTID: TEXTUAL COOCCURRENCE #
+#####################################################
+
+#           d_COUNTS                                                  d      d_BOOL
+# context_id
+# 1061             8  {(33025, 33025), (33027, 33027), (32998, 32998...        True
+# 545              4  {(16933, 16933), (16935, 16935), (16919, 16919...        True
+# 6815             4  {(132976, 132976), (132969, 132969), (132978, ...        True
+# 6548             4  {(123407, 123407), (123394, 123394), (123409, ...        True
+# 6986             4  {(139381, 139381), (139383, 139383), (139396, ...        True
+# ...            ...                                                ...         ...
+# 2259             1                                   {(79339, 79339)}        True
+# 1158             1                                   {(36252, 36252)}        True
+# 2201             1                                   {(77579, 77579)}        True
+# 6636             1                                 {(126185, 126185)}        True
+# 1529             1                                   {(51388, 51388)}        True
+
+
+class Discourseme():
+
+    def __init__(self, cwb_id, name, df):
+        """
+        :param str name: name of the discourseme
+        :param DataFrame df:
+        """
+        pass
+
+    def breakdown(self, p_atts):
+        pass
+
+    def concordance(self, context, p_atts, form):
+        pass
+
+    def meta(self, s_atts):
+        pass
+
+    def collocates(self, context, form):
+        pass
+
+
+class Constellation_:
+
+    # sqlite database
+    cwb_id = ""
+    name = ""
+    match = 0
+    matchend = 1
+
+    def add_discourseme(self, name, df):
+        pass
+
+    def breakdown(self, p_atts=['lemma'], flags='', form='dataframe'):
+        pass
+
+    def concordance(self, names=None, p_atts=['word'], s_atts=[],
+                    s_context=None, context_left=10, context_right=10,
+                    order='random', random_seed=42, number=100,
+                    form='dataframe'):
+        """
+        filtered by names (None=all)
+        kwic-focus on match, matchend of each name
+        other discoursemes are highlighted
+        """
+        pass
+
+    def meta(self, s_att, pos='match', form='dataframe'):
+        """
+        dataframe: s_att_value, frequency, ipm
+        """
+
+        # # init corpus
+        # corpus = Corpus(corpus_name, lib_path, cqp_bin, registry_path, data_path)
+
+        # # init discourseme constellation
+        # topic_query = format_cqp_query(topic_items,
+        #                                p_query=p_query, s_query=s_query,
+        #                                flags=flags_query, escape=escape)
+        # dump = corpus.query(topic_query, context=None)
+        # # get meta data
+        # meta = dump.concordance(
+        #     s_show=s_show, form='simple', order=order, cut_off=cut_off
+        # )
+
+        # # tabulate
+        # output = list()
+        # for s in s_show:
+
+        #     # tabulate number of tokens
+        #     # TODO: no need to calculate for each text_x, text_y, etc.
+        #     lengths = corpus.query_s_att(s)
+        #     lengths = lengths.df.reset_index()
+        #     lengths['nr_tokens'] = lengths['matchend'] - lengths['match'] + 1
+
+        #     # absolute frequency in each subcorpus
+        #     m = meta[s].value_counts()
+
+        #     if len(m) < 100:
+        #         m = m.to_frame()
+        #         m.columns = ['frequency']
+        #         ell = lengths.groupby(s)[['nr_tokens']].agg(sum)
+        #         ell = ell.join(m, how='outer')
+        #         ell = ell.fillna(0, downcast='infer')
+        #         ell['IPM'] = round(ell['frequency'] / ell['nr_tokens'] * 10**6, 2)
+        #         ell.index.name = s
+        #         ell = ell.reset_index()
+        #         output.append(ell.to_html(index=False, bold_rows=False))
+
+        pass
 
+    def collocates(self, window=5, p_atts=['lemma'], flags='',
+                   ams=None, order='log_likelihood', cut_off=None, form='dataframe',
+                   reference=None):
+        """
+        if reference is None: reference are marginals
+        """
+        pass
 
-# TODO discourseme definition: items, queries, corpora
-class Discourseme:
-    def __init__(self):
+    def associations(self, reference=None):
+        """
+        if reference is None: reference are marginals
+        """
         pass
 
 
@@ -99,7 +214,7 @@ def __init__(self, dump, name='topic'):
         param str name: name of the node
         """
 
-        self.corpus = dump.corpus
+        self.corpus = dump
         # init with given dump
         self.df = dump.df[['contextid', 'context', 'contextend']].astype("Int64")
         # init added discoursemes
@@ -150,7 +265,7 @@ def breakdown(self, p_atts=['word'], flags=""):
     def concordance(self, window=5,
                     p_show=['word', 'lemma'], s_show=[],
                     order='random', cut_off=100, random_seed=42,
-                    htmlify_meta=False):
+                    htmlify_meta=False, cwb_ids=True, form='dict'):
         """Retrieve concordance lines for constellation.
 
         :param int window: cpos further away from node will be marked 'out_of_window'
@@ -163,15 +278,16 @@ def concordance(self, window=5,
         hkeys = list(self.discoursemes.keys())
         df = group_lines(self.df, hkeys)
         conc = Concordance(self.corpus.copy(), df)
-        lines = conc.lines(form='dict', p_show=p_show, s_show=s_show, order=order, cut_off=cut_off)
-        output = lines.apply(lambda row: format_roles(row, hkeys, s_show, window, htmlify_meta=True), axis=1)
+        lines = conc.lines(form=form, p_show=p_show, s_show=s_show, order=order, cut_off=cut_off, cwb_ids=cwb_ids)
 
-        return list(output)
+        if form == 'dict':
+            lines = list(lines.apply(lambda row: format_roles(row, hkeys, s_show, window, htmlify_meta=htmlify_meta), axis=1))
 
-    def collocates(self, windows=[3, 5, 7],
-                   p_show=['lemma'], flags=None,
-                   ams=None, frequencies=True,
-                   min_freq=2, order='log_likelihood', cut_off=None,
+        return lines
+
+    def collocates(self, windows=[3, 5, 7], p_show=['lemma'],
+                   flags=None, ams=None, min_freq=2,
+                   order='log_likelihood', cut_off=None,
                    marginals='corpus'):
         """Retrieve collocates for constellation.
 
@@ -181,28 +297,49 @@ def collocates(self, windows=[3, 5, 7],
         :rtype: list of DataFrames
         """
 
-        # get relevant contexts
-        df_dump = self.df.drop_duplicates(subset=['context', 'contextend'])
-        df_cooc, f1_set = dump2cooc(df_dump)
+        # get df_cooc, f1_set, node_freq from cache if possible
+        identifier = generate_idx(
+            [self.df.reset_index()[['match', 'matchend', 'context', 'contextend']], p_show, min_freq, self.discoursemes.keys()]
+        )
+        f1_set = self.corpus.cache.get(identifier + "-f1_set")
+        df_cooc = self.corpus.cache.get(identifier + "-df_cooc")
+        node_freq = self.corpus.cache.get(identifier + "-node_freq")
+
+        # create and cache otherwise
+        if not isinstance(df_cooc, DataFrame):
+
+            # get relevant contexts
+            logger.info('create cooc matrix')
+            df_dump = self.df.drop_duplicates(subset=['context', 'contextend'])
+            df_cooc, f1_set = dump2cooc(df_dump)
 
-        logger.info('get cpos that are consumed by discoursemes')
-        for idx in self.discoursemes.keys():
-            f1_set.update(self.discoursemes[idx].matches())
-        df_cooc = df_cooc.loc[~df_cooc['cpos'].isin(f1_set)]
+            logger.info('get cpos that are consumed by discoursemes')
+            for idx in self.discoursemes.keys():
+                f1_set.update(self.discoursemes[idx].matches())
+            df_cooc = df_cooc.loc[~df_cooc['cpos'].isin(f1_set)]
 
-        # count node freqs
-        node_freq = self.corpus.counts.cpos(f1_set, p_show)
+            # count node freqs
+            node_freq = self.corpus.counts.cpos(f1_set, p_show)
 
         # determine collocates
         collocates = Collocates(
-            corpus=self.corpus, df_dump=None, p_query=p_show, mws=max(windows),
-            df_cooc=df_cooc, f1_set=f1_set, node_freq=node_freq
+            corpus=self.corpus.copy(),
+            df_dump=None,
+            p_query=p_show,
+            mws=max(windows),
+            df_cooc=df_cooc,
+            f1_set=f1_set,
+            node_freq=node_freq
         )
         output = dict()
         for window in windows:
             output[window] = collocates.show(
-                window=window, order=order, cut_off=cut_off, ams=ams,
-                min_freq=min_freq, frequencies=frequencies, flags=flags,
+                window=window,
+                order=order,
+                cut_off=cut_off,
+                ams=ams,
+                min_freq=min_freq,
+                flags=flags,
                 marginals=marginals
             )
 
@@ -222,7 +359,7 @@ def __init__(self, dump, s_context, name='topic'):
             name = list(dumps.keys())[0]
             dump = dumps.pop(name)
 
-        elif isinstance(dump, Dump):
+        elif isinstance(dump, SubCorpus):
             dumps = {}
 
         else:
@@ -230,7 +367,7 @@ def __init__(self, dump, s_context, name='topic'):
 
         # create
         self.s_context = s_context
-        self.corpus = dump.corpus
+        self.corpus = dump
         self.N = len(self.corpus.attributes.attribute(s_context, 's'))
 
         # init discoursemes
@@ -300,24 +437,27 @@ def concordance(self, window=0, p_show=['word', 'lemma'],
         # ACTUAL CONCORDANCING
         conc = Concordance(self.corpus.copy(), df)
         lines = conc.lines(form='dict', p_show=p_show, s_show=s_show, order=order, cut_off=cut_off)
-        output = lines.apply(lambda row: format_roles(row, names, s_show=names_bool+s_show, window=0, htmlify_meta=True), axis=1)
+        output = lines.apply(lambda row: format_roles(
+            row, names, s_show=names_bool+s_show, window=0, htmlify_meta=htmlify_meta
+        ), axis=1)
 
         return list(output)
 
-    def associations(self, ams=None, frequencies=True,
-                     min_freq=2, order='log_likelihood',
-                     cut_off=None):
+    def associations(self, ams=None, min_freq=2,
+                     order='log_likelihood', cut_off=None):
 
         counts = self.df[[c for c in self.df.columns if c.endswith("_BOOL")]]
         counts.columns = [c.split("_BOOL")[0] for c in counts.columns]
         cooc = counts.fillna(False)
 
         # TODO triangular matrix
-        tables = DataFrame()
+        tables = list()
         for name in counts.columns:
+            # TODO super slow for pandas 2.0, but why?
             table = round(textual_associations(cooc, self.N, name).reset_index(), 2)
             table['node'] = name
-            tables = concat([tables, table])
+            tables.append(table)
+        tables = concat(tables)
 
         tables = tables[
             ['node', 'candidate'] + [d for d in tables.columns if d not in ['node', 'candidate']]
@@ -350,10 +490,6 @@ def textual_associations(cooc, N, column):
     return contingencies
 
 
-########################################################
-# CONSTELLATION CREATION FROM DISCOURSEMES AND QUERIES
-# TODO parallelize
-########################################################
 def create_constellation(corpus_name,
                          # discoursemes
                          topic_discourseme,
@@ -362,19 +498,20 @@ def create_constellation(corpus_name,
                          # context settings
                          s_context,
                          context=20,
-                         # query settings
+                         window=None,
+                         approximate=False,
+                         # CWB settings
+                         match_strategy='longest',
+                         lib_dir=None,
+                         cqp_bin='cqp',
+                         registry_dir='/usr/local/share/cwb/registry/',
+                         data_dir=None,
+                         # for creating from items rather than queries
+                         querify=True,
                          p_query='word',
                          s_query=None,
                          flags='%cd',
-                         escape=True,
-                         match_strategy='longest',
-                         # CWB settings
-                         lib_path=None,
-                         cqp_bin='cqp',
-                         registry_path='/usr/local/share/cwb/registry/',
-                         data_path='/tmp/ccc-data/',
-                         window=None,
-                         approximate=False):
+                         escape=True):
     """simple constellation creator. returns a Constellation() if a
     topic_discourseme is given, otherwise a TextConstellation(). Note
     that for TextConstellations, there is no difference between
@@ -387,124 +524,39 @@ def create_constellation(corpus_name,
 
     """
 
-    # pre-process parameters
-    s_context = s_query if not s_context else s_context
-    s_query = s_context if s_query is None else s_query
+    # translate items into queries?
+    if querify:
 
-    # init corpus
-    corpus = Corpus(corpus_name, lib_path, cqp_bin, registry_path, data_path)
+        # copy so local changes don't affect input
+        topic_discourseme = topic_discourseme.copy()
+        filter_discoursemes = filter_discoursemes.copy()
+        additional_discoursemes = additional_discoursemes.copy()
 
-    # topic -> Constellation()
-    if len(topic_discourseme) > 0:
-
-        if len(topic_discourseme) > 1:
-            raise ValueError("only one topic discourseme can be given")
+        # pre-process parameters
+        s_context = s_query if not s_context else s_context
+        s_query = s_context if s_query is None else s_query
 
-        # init with topic
-        topic_name = list(topic_discourseme.keys())[0]
-        topic_items = topic_discourseme[topic_name]
-        topic_query = format_cqp_query(
-            topic_items, p_query=p_query, s_query=s_query, flags=flags, escape=escape
-        )
-        topic_dump = corpus.query_cqp(
-            topic_query,
-            context=context,
-            context_break=s_context,
-            match_strategy=match_strategy
-        )
-        const = Constellation(topic_dump, topic_name)
-
-        if approximate:
-            sub = topic_dump.df.set_index(['context', 'contextend'])
-            corpus = corpus.activate_subcorpus(nqr='TempRestriction', df_dump=sub)
-
-        # add filter discoursemes
-        for disc_name, disc_items in filter_discoursemes.items():
+        # create queries
+        for disc_name, disc_items in topic_discourseme.items():
             disc_query = format_cqp_query(
                 disc_items, p_query=p_query, s_query=s_query, flags=flags, escape=escape
             )
-            disc_dump = corpus.query_cqp(
-                disc_query,
-                context=None,
-                context_break=s_context,
-                match_strategy=match_strategy
-            )
-            if len(disc_dump.df) > 0:
-                const.add_discourseme(disc_dump, disc_name, drop=True, window=window)
+            topic_discourseme[disc_name] = disc_query
 
-        # add additional discoursemes
-        for disc_name, disc_items in additional_discoursemes.items():
+        for disc_name, disc_items in filter_discoursemes.items():
             disc_query = format_cqp_query(
                 disc_items, p_query=p_query, s_query=s_query, flags=flags, escape=escape
             )
-            disc_dump = corpus.query_cqp(
-                disc_query,
-                context=None,
-                context_break=s_context,
-                match_strategy=match_strategy
-            )
-            if len(disc_dump.df) > 0:
-                const.add_discourseme(disc_dump, disc_name, drop=False)
+            filter_discoursemes[disc_name] = disc_query
 
-        corpus = corpus.activate_subcorpus()
-
-    # no topic -> TextConstellation()
-    else:
-
-        # no filter implemented: all discoursemes are equal
-        discoursemes = {**filter_discoursemes, **additional_discoursemes}
-
-        # init with arbitrary topic
-        topic_name = list(discoursemes.keys())[0]
-        topic_items = discoursemes.pop(topic_name)
-        topic_query = format_cqp_query(
-            topic_items, p_query=p_query, s_query=s_query, flags=flags, escape=escape
-        )
-        topic_dump = corpus.query_cqp(
-            topic_query,
-            context=context,
-            context_break=s_context,
-            match_strategy=match_strategy
-        )
-        const = TextConstellation(topic_dump, s_context, topic_name)
-
-        # add further discoursemes
-        for disc_name, disc_items in discoursemes.items():
+        for disc_name, disc_items in additional_discoursemes.items():
             disc_query = format_cqp_query(
                 disc_items, p_query=p_query, s_query=s_query, flags=flags, escape=escape
             )
-            disc_dump = corpus.query_cqp(
-                disc_query,
-                context=None,
-                context_break=s_context,
-                match_strategy=match_strategy
-            )
-            const.add_discourseme(disc_dump, disc_name)
-
-    return const
-
-
-def create_constellation_query(corpus_name,
-                               # discoursemes
-                               topic_discourseme,
-                               filter_discoursemes,
-                               additional_discoursemes,
-                               # context settings
-                               s_context,
-                               context=20,
-                               # CWB settings
-                               match_strategy='longest',
-                               lib_path=None,
-                               cqp_bin='cqp',
-                               registry_path='/usr/local/share/cwb/registry/',
-                               data_path='/tmp/ccc-data/',
-                               window=None):
-    """same as above, but with pre-formatted CQP queries
-
-    """
+            additional_discoursemes[disc_name] = disc_query
 
     # init corpus
-    corpus = Corpus(corpus_name, lib_path, cqp_bin, registry_path, data_path)
+    corpus = Corpus(corpus_name, lib_dir, cqp_bin, registry_dir, data_dir)
 
     # topic -> Constellation()
     if len(topic_discourseme) > 0:
@@ -523,6 +575,10 @@ def create_constellation_query(corpus_name,
         )
         const = Constellation(topic_dump, topic_name)
 
+        if approximate:
+            sub = topic_dump.df.set_index(['context', 'contextend'])
+            corpus = corpus.subcorpus('TempRestriction', df_dump=sub)
+
         # add filter discoursemes
         for disc_name, disc_query in filter_discoursemes.items():
             disc_dump = corpus.query_cqp(
@@ -531,7 +587,8 @@ def create_constellation_query(corpus_name,
                 context_break=s_context,
                 match_strategy=match_strategy
             )
-            const.add_discourseme(disc_dump, disc_name, drop=True, window=window)
+            if len(disc_dump.df) > 0:
+                const.add_discourseme(disc_dump, disc_name, drop=True, window=window)
 
         # add additional discoursemes
         for disc_name, disc_query in additional_discoursemes.items():
@@ -541,7 +598,8 @@ def create_constellation_query(corpus_name,
                 context_break=s_context,
                 match_strategy=match_strategy
             )
-            const.add_discourseme(disc_dump, disc_name, drop=False)
+            if len(disc_dump.df) > 0:
+                const.add_discourseme(disc_dump, disc_name, drop=False)
 
     # no topic -> TextConstellation()
     else:
@@ -568,6 +626,9 @@ def create_constellation_query(corpus_name,
                 context_break=s_context,
                 match_strategy=match_strategy
             )
-            const.add_discourseme(disc_dump, disc_name)
+            if len(disc_dump.df) > 0:
+                const.add_discourseme(disc_dump, disc_name)
 
     return const
+
+
diff --git a/ccc/dumps.py b/ccc/dumps.py
index 395b452..c7bde2f 100644
--- a/ccc/dumps.py
+++ b/ccc/dumps.py
@@ -7,186 +7,13 @@
 """
 import logging
 
-# requirements
-from pandas import DataFrame
+from pandas import DataFrame, concat
 
-# part of module
 from .collocates import Collocates
-from .concordances import Concordance
-from .keywords import Keywords
-from .utils import correct_anchors, fold_df, merge_intervals
 
 logger = logging.getLogger(__name__)
 
 
-class Dump:
-    """ result of a query """
-
-    def __init__(self, corpus, df_dump, name_cqp):
-
-        self.df = df_dump
-        self.corpus = corpus
-        self.name_cqp = name_cqp
-        self.size = len(self.df)
-
-        self._matches = None
-        self._context = None
-
-    def __str__(self):
-
-        desc = [f'a ccc.Dump with {self.size} matches']
-        if self.corpus.subcorpus:
-            crpssbcrps = self.corpus.corpus_name + ":" + self.corpus.subcorpus
-        else:
-            crpssbcrps = self.corpus.corpus_name
-        desc.append(f'- corpus "{crpssbcrps}" ({self.corpus.corpus_size} tokens in whole corpus)')
-        if self.name_cqp is not None:
-            desc.append(f'- name in cqp: "{self.name_cqp}"')
-        desc.append(f'- columns: {list(self.df.columns)}')
-        return "\n".join(desc)
-
-    def __repr__(self):
-        """Info string.
-
-        """
-        return self.__str__()
-
-    def set_context(self, context=None, context_break=None,
-                    context_left=None, context_right=None):
-        """Set context in the dump.
-
-        """
-        # pre-process context
-        if context_left is None:
-            context_left = context
-        if context_right is None:
-            context_right = context
-
-        # set context
-        df = self.corpus.dump2context(
-            self.df, context_left, context_right, context_break
-        )
-
-        return Dump(self.corpus, df, self.name_cqp)
-
-    def correct_anchors(self, corrections):
-        """Correct anchors by integer offsets.
-
-        """
-        self.df = correct_anchors(self.df, corrections)
-
-    def breakdown(self, p_atts=['word'], flags=""):
-        """Frequency breakdown of match..matchend.
-
-        """
-
-        logger.info('creating frequency breakdown')
-        breakdown = self.corpus.counts.dump(
-            df_dump=self.df,
-            start='match', end='matchend',
-            p_atts=p_atts, strategy=1
-        )
-
-        breakdown = fold_df(breakdown, flags)
-
-        return breakdown
-
-    def matches(self):
-        """
-        :return: cpos of (match .. matchend) regions
-        :rtype: set
-        """
-        if self._matches is None:
-            f1 = set()
-            for match, matchend in self.df.index:
-                f1.update(range(match, matchend + 1))
-            self._matches = f1
-
-        return self._matches
-
-    def context(self):
-        """
-        :return: cpos of (context .. contextend) regions including matches
-        :rtype: DataFrame
-        """
-        if self._context is None:
-            self._context = DataFrame.from_records(merge_intervals(
-                self.df[['context', 'contextend']].values.tolist()
-            ), columns=['context', 'contextend'])
-
-        return self._context
-
-    def marginals(self, start='match', end='matchend', p_atts=['word']):
-
-        return self.corpus.counts.dump(
-            self.df, start=start, end=end, p_atts=p_atts, split=True
-        )
-
-    def concordance(self, form='simple', p_show=['word'], s_show=[],
-                    order='first', cut_off=100, matches=None,
-                    slots=None, cwb_ids=False):
-
-        conc = Concordance(
-            self.corpus.copy(),
-            df_dump=self.df
-        )
-
-        return conc.lines(
-            form=form,
-            p_show=p_show,
-            s_show=s_show,
-            order=order,
-            cut_off=cut_off,
-            matches=matches,
-            slots=slots,
-            cwb_ids=cwb_ids
-        )
-
-    def collocates(self, p_query=['lemma'], mws=20, window=5, order='O11',
-                   cut_off=100, ams=None, min_freq=2,
-                   frequencies=True, flags=None, marginals='corpus',
-                   show_negative=False):
-
-        mws = max(mws, window)
-
-        coll = Collocates(
-            self.corpus.copy(),
-            df_dump=self.df,
-            p_query=p_query,
-            mws=mws
-        )
-
-        return coll.show(
-            window=window,
-            order=order,
-            cut_off=cut_off,
-            ams=ams,
-            min_freq=min_freq,
-            frequencies=frequencies,
-            flags=flags,
-            marginals=marginals,
-            show_negative=show_negative
-        )
-
-    def keywords(self, p_query=['lemma'], order='O11', cut_off=100,
-                 ams=None, min_freq=2, frequencies=True, flags=None):
-
-        kw = Keywords(
-            self.corpus.copy(),
-            self.df,
-            p_query
-        )
-
-        return kw.show(
-            order=order,
-            cut_off=cut_off,
-            ams=ams,
-            min_freq=min_freq,
-            frequencies=frequencies,
-            flags=flags
-        )
-
-
 class Dumps:
     """ collection of dumps, i.e. query matches in different subcorpora """
 
@@ -207,40 +34,44 @@ def __init__(self, corpus, s_dict, s_att='text_id'):
         for i, s in enumerate(self.s_dict.keys()):
             logger.info(f"... subcorpus {i+1} of {len(s_dict)}")
             df_dump = df_spans.loc[df_spans[s_att].isin(s_dict[s])]
-            dumps[s] = Dump(corpus.copy(), df_dump, name_cqp=None)
+            dumps[s] = corpus.subcorpus(None, df_dump)
 
         self.dumps = dumps
 
-    def keywords(self, p_query=['lemma'], order='log_likelihood', cut_off=100,
-                 ams=None, min_freq=2, frequencies=True, flags=None,
-                 subset=None):
+    def keywords(self, p_query=['lemma'], order='log_likelihood',
+                 cut_off=100, ams=None, min_freq=2, flags=None,
+                 subset=None, marginals='corpus', show_negative=False):
         """
         :return: dictionary of {subcorpus_name: table}
         :rtype: dict
         """
+        # TODO: multiproc
 
         subset = list(self.s_dict.keys()) if subset is None else subset
 
         logger.info("computing keyword tables ...")
-        # TODO: multiproc
-        tables = dict()
-        i = 0
-        for s in subset:
-            i += 1
-            logger.info(f"... table {i} of {len(subset)}")
+        tables = list()
+        for i, s in enumerate(subset):
+            logger.info(f"... table {i+1} of {len(subset)}")
             dump = self.dumps[s]
-            tables[s] = dump.keywords(
+            table = dump.keywords(
                 p_query=p_query, order=order, cut_off=cut_off,
-                ams=ams, min_freq=min_freq,
-                frequencies=frequencies, flags=flags
+                ams=ams, min_freq=min_freq, flags=flags, marginals=marginals,
+                show_negative=show_negative
             )
+            if len(table) > 0:
+                table['subcorpus'] = s
+                tables.append(table.reset_index())
+
+        df = concat(tables).reset_index(drop=True).set_index(['subcorpus', 'item'])
 
-        return tables
+        return df
 
     def collocates(self, cqp_query, window=5, p_query=['lemma'],
-                   order='log_likelihood', cut_off=100, ams=None, min_freq=2,
-                   frequencies=True, flags=None, subset=None, context_break=None,
-                   reference='local'):
+                   order='log_likelihood', cut_off=100, ams=None,
+                   min_freq=2, flags=None, subset=None,
+                   context_break=None, marginals='corpus',
+                   show_negative=False):
         """
         reference:
         .. local: window freq. compared to marginals of subcorpus (excl. nodes)
@@ -251,6 +82,7 @@ def collocates(self, cqp_query, window=5, p_query=['lemma'],
         :return: dictionary of {subcorpus_name: table}
         :rtype: dict
         """
+        # TODO: multiproc
 
         subset = list(self.s_dict.keys()) if subset is None else subset
         context_break = self.s_att if context_break is None else context_break
@@ -264,22 +96,25 @@ def collocates(self, cqp_query, window=5, p_query=['lemma'],
         df_glob = self.corpus.dump2satt(dump_glob, self.s_att)
 
         logger.info("computing collocate tables ...")
-        # TODO: multiproc
-        tables = dict()
-        i = 0
-        for s in subset:
-            i += 1
-            logger.info(f"... table {i} of {len(subset)}")
+        tables = list()
+        for i, s in enumerate(subset):
+            logger.info(f"... table {i+1} of {len(subset)}")
 
             # determine reference frequencies
-            if isinstance(reference, str):
-                if reference == 'local':
+            if isinstance(marginals, str):
+                if marginals == 'local':
                     # get local marginals
+                    # THIS is expensive, use subcorpus.marginals instead!
                     marginals = self.corpus.counts.dump(
                         self.dumps[s].df, split=True, p_atts=p_query
                     )
-                elif reference == 'global':
-                    marginals = 'corpus'
+                    # marginals = self.corpus.marginals(p_atts=p_query)
+                elif marginals == 'corpus':
+                    pass
+                else:
+                    raise NotImplementedError()
+            elif not isinstance(marginals, DataFrame):
+                raise NotImplementedError()
 
             # create collocates table
             df_loc = df_glob.loc[
@@ -288,10 +123,14 @@ def collocates(self, cqp_query, window=5, p_query=['lemma'],
             collocates = Collocates(
                 self.corpus, df_loc, p_query=p_query, mws=window
             )
-            tables[s] = collocates.show(
+            table = collocates.show(
                 window=window, order=order, cut_off=cut_off,
-                ams=ams, min_freq=min_freq, frequencies=frequencies,
-                flags=flags, marginals=marginals
+                ams=ams, min_freq=min_freq, flags=flags, marginals=marginals
             )
+            if len(table) > 0:
+                table['subcorpus'] = s
+                tables.append(table.reset_index())
+
+        df = concat(tables).reset_index(drop=True).set_index(['subcorpus', 'item'])
 
-        return tables
+        return df
diff --git a/ccc/keywords.py b/ccc/keywords.py
index c557125..03feb3e 100644
--- a/ccc/keywords.py
+++ b/ccc/keywords.py
@@ -12,44 +12,59 @@
 from pandas import DataFrame
 
 # part of module
+from .cache import generate_idx
 from .counts import score_counts
 
 logger = logging.getLogger(__name__)
 
 
-# TODO re-write as regular methods
 class Keywords:
     """ keyword analysis """
 
-    def __init__(self, corpus, df_dump, p_query=['lemma']):
-
-        self.corpus = corpus
-
-        # activate dump
-        self.size = len(df_dump)
+    def __init__(self, corpus, df_dump, p_query=['lemma'],
+                 counts=None):
 
         # consistency check
-        if self.size == 0:
-            logger.warning('cannot calculate keywords on 0 regions')
+        if df_dump is not None and len(df_dump) == 0:
+            logger.warning('empty dump')
             return
 
+        # init corpus
+        self.corpus = corpus
+
         # determine layer to work on
         self.p_query = [p_query] if isinstance(p_query, str) else p_query
-        available_attributes = self.corpus.attributes_available['attribute'].values
+        available_attributes = self.corpus.available_attributes()['attribute'].values
         if not set(self.p_query).issubset(set(available_attributes)):
             logger.warning('specfied p-attribute(s) ("%s") not available' % " ".join(self.p_query))
             logger.warning('falling back to primary layer')
             self.p_query = ['word']
 
         # collect context and save result
-        logger.info('collecting token counts of subcorpus')
-        self.counts = corpus.counts.dump(
-            df_dump, start='match', end='matchend', p_atts=self.p_query, split=True
-        )
+        if df_dump is not None:
+
+            # get from cache if possible
+            identifier = generate_idx([df_dump.reset_index()[['match', 'matchend']], p_query])
+            counts = self.corpus.cache.get(identifier + "-matchcounts")
+
+            # create and cache otherwise
+            if not isinstance(counts, DataFrame):
+                logger.info('collecting token counts of subcorpus')
+                counts = corpus.counts.dump(
+                    df_dump, start='match', end='matchend', p_atts=self.p_query, split=True
+                )
+                self.corpus.cache.set(identifier + "-matchcounts", counts)
+
+        else:
+            if counts is None:
+                logger.error('if no dump is given, you have to provide all counts')
+                return
+
+        self.counts = counts
 
     def show(self, order='log_likelihood', cut_off=100, ams=None,
-             min_freq=2, frequencies=True, flags=None,
-             marginals='corpus', show_negative=False):
+             min_freq=2, flags=None, marginals='corpus',
+             show_negative=False):
 
         # consistency check
         if self.counts.empty:
@@ -77,7 +92,7 @@ def show(self, order='log_likelihood', cut_off=100, ams=None,
 
         # create dataframe
         f2 = marginals[['freq']].rename(columns={'freq': 'f2'})
-        df = f2.join(f)
+        df = f.join(f2, how='left')
         df = df.fillna(0)
         df['f1'] = f1
         df['N'] = N
@@ -86,43 +101,54 @@ def show(self, order='log_likelihood', cut_off=100, ams=None,
         keywords = score_counts(df, order=order, cut_off=cut_off,
                                 flags=flags, ams=ams, vocab=vocab)
 
-        if frequencies:
-            # throw away anti-keywords by default
-            if not show_negative:
-                keywords = keywords.loc[keywords['O11'] >= keywords['E11']]
+        # throw away anti-keywords by default
+        if not show_negative:
+            keywords = keywords.loc[keywords['O11'] >= keywords['E11']]
 
         return keywords
 
 
-def keywords(corpus, corpus_reference, p, p_reference, order='O11',
-             cut_off=100, ams=None, min_freq=2, frequencies=True, flags=None):
+def keywords(corpus, corpus_reference, p=['lemma'],
+             p_reference=['lemma'], order='O11', cut_off=100,
+             ams=None, min_freq=2, flags=None):
     """directly extract keywords by comparing two (sub-)corpora
 
     :param ccc.Corpus corpus: target corpus
     :param ccc.Corpus corpus_reference: reference corpus
-    :param str p: p-att
-    :param str p_reference: p-att
+    :param list p: p-att (str possible)
+    :param list p_reference: p-att (str possible)
     :param str order: AM to order
     :param list ams: list of AMs
     :param int min_freq: minimum number of occurrences in target corpus
-    :param bool frequencies: add frequency columns
     :param list flags: '%cd'
 
     """
 
-    # get and merge both dataframes of counts
-    left = corpus.marginals(p_atts=p)[['freq']].rename(columns={'freq': 'f1'})
-    right = corpus_reference.marginals(p_atts=p_reference)[['freq']].rename(columns={'freq': 'f2'})
-    df = left.join(right, how='outer')
-    df['N1'] = left['f1'].sum()
-    df['N2'] = right['f2'].sum()
-    df = df.fillna(0, downcast='infer')
-    logger.info('cut-off')
-    vocab = len(df)
-    df = df.loc[df['f1'] >= min_freq]
-
-    # score counts
-    keywords = score_counts(df, order=order, cut_off=cut_off,
-                            flags=flags, ams=ams, digits=4, vocab=vocab)
+    # get from cache if possible
+    identifier = generate_idx([corpus.__str__(), corpus_reference.__str__(),
+                               p, p_reference, order, cut_off, ams, min_freq, flags], prefix='kw-')
+    keywords = corpus.cache.get(identifier)
+
+    if keywords is None:
+        # get and merge both dataframes of counts
+        logger.info('starting keyword analysis')
+        target = corpus.marginals(p_atts=p)[['freq']].rename(columns={'freq': 'f1'})
+        reference = corpus_reference.marginals(p_atts=p_reference)[['freq']].rename(columns={'freq': 'f2'})
+
+        logger.info('-- combining frequency lists')
+        df = target.join(reference, how='outer')
+        df['N1'] = target['f1'].sum()
+        df['N2'] = reference['f2'].sum()
+        df = df.fillna(0, downcast='infer')
+
+        logger.info('-- cut-off')
+        vocab = len(df)
+        df = df.loc[df['f1'] >= min_freq]
+
+        # score counts
+        keywords = score_counts(df, order=order, cut_off=cut_off,
+                                flags=flags, ams=ams, digits=4, vocab=vocab)
+        logger.info('-- done with keyword analysis')
+        corpus.cache.set(identifier, keywords)
 
     return keywords
diff --git a/ccc/utils.py b/ccc/utils.py
index 50166d6..83b7bef 100644
--- a/ccc/utils.py
+++ b/ccc/utils.py
@@ -198,6 +198,17 @@ def preprocess_query(query):
     }
 
 
+def decode(text):
+    """savely decode a string catching common errors
+
+    """
+    try:
+        text = text.decode('utf-8')
+    except (UnicodeDecodeError, AttributeError):
+        text = ""
+    return text
+
+
 #################################
 # working on nodes and contexts #
 #################################
@@ -229,17 +240,57 @@ def _node2cotext(match, matchend, context, contextend):
 
 
 def merge_intervals(inter, start_index=0):
-    """ for merging contexts """
+    """for merging contexts; union of intervals. intervals can have
+    arbitrary overlaps, but must be sorted by left boundaries
+
+    :param list inter: list of intervals (each one a pair of left and right boundary)
+    :param int start_index: for recursive application
+
+    :return: list of merged intervals
+    :rtype: list
+    """
+
     for i in range(start_index, len(inter)-1):
         if inter[i][1] >= inter[i+1][0]:
             new_start = inter[i][0]
-            new_end = inter[i+1][1]
+            new_end = max(inter[i][1], inter[i+1][1])
             inter[i] = [new_start, new_end]
             del inter[i+1]
             return merge_intervals(inter.copy(), start_index=i)
     return inter
 
 
+def intersect_intervals(inter_left, inter_right):
+    """for merging matches; intersection of intervals. intervals can have
+    arbitrary overlaps, but must be sorted by left boundaries.
+
+    :param list inter_left: list of intervals (each one a pair of left and right boundary + ...)
+    :param list inter_right: list of intervals (each one a pair of left and right boundary + ...)
+    """
+
+    i = j = 0                   # indexes of inter_left and inter_right
+    n = len(inter_left)
+    m = len(inter_right)
+
+    intervals = list()          # output variable
+    while i < n and j < m:
+
+        # compare inter_left[i] with inter_right[j]
+        start = max(inter_left[i][0], inter_right[j][0])
+        end = min(inter_left[i][1], inter_right[j][1])
+        if start <= end:
+            intervals.append([start, end] + inter_left[i][2:] + inter_right[j][2:])
+
+        # if inter_left[i]'s right bound is smaller than inter_right[j]'s right bound: increment i
+        if inter_left[i][1] < inter_right[j][1]:
+            i += 1
+        # else increment j
+        else:
+            j += 1
+
+    return intervals
+
+
 def calculate_offset(row):
     """ calculates offset of y to x """
 
@@ -420,9 +471,10 @@ def group_lines(df, names):
 
     """
 
-    df = df.copy()
     # TODO: deduplication necessary?
+    df = df.copy()
     df_reduced = df[~df.index.duplicated(keep='first')][['contextid', 'context', 'contextend']]
+
     for name in names:
         columns = [m + "_" + name for m in ['offset', 'match', 'matchend']]
         df[name] = df[columns].values.tolist()
@@ -439,12 +491,15 @@ def aggregate_matches(df, name, context_col='contextid',
     convert dataframe:
     === (m, me) ci ===
     to
-    === (ci) {name} COUNTS_{name} BOOL_{name} ===
+    === (ci) {name} {name}_COUNTS {name}_BOOL ===
 
     """
+
     # counts
     counts = DataFrame(df[context_col].value_counts()).astype("Int64")
     counts.columns = [name + '_COUNTS']
+    counts.index.name = context_col
+    counts = counts.sort_values(by=context_col)
 
     # matches
     matches = df.reset_index()
@@ -456,8 +511,7 @@ def aggregate_matches(df, name, context_col='contextid',
     table = counts.join(matches)
 
     # bool
-    table[name + '_BOOL'] = table[name + '_COUNTS'] > 0
-    table[name + '_BOOL'] = table[name + '_BOOL'].fillna(False)
+    table[name + '_BOOL'] = (table[name + '_COUNTS'] > 0).fillna(False)
 
     return table
 
diff --git a/ccc/version.py b/ccc/version.py
index c8c5d85..f8085a1 100644
--- a/ccc/version.py
+++ b/ccc/version.py
@@ -4,4 +4,4 @@
 
 """
 
-__version__ = "0.11.8"
+__version__ = "0.12.0"
diff --git a/demos/readme.py b/demos/readme.py
index 43644b5..e93df1b 100644
--- a/demos/readme.py
+++ b/demos/readme.py
@@ -1,10 +1,12 @@
 """readme.py: create tables for the README.md"""
 
-import pyperclip
 import sys
+
+import pyperclip
+
 sys.path.append("/home/ausgerechnet/implementation/cwb-ccc/")
 import ccc
-from ccc import Corpus, Corpora
+from ccc import Corpora, Corpus
 
 print(ccc.__version__)
 
diff --git a/makefile b/makefile
index 71012bf..3f63fa8 100644
--- a/makefile
+++ b/makefile
@@ -11,6 +11,9 @@ benchmark:
 coverage:
 	pipenv run pytest --cov-report term-missing -v --cov=ccc/
 
+requirements:
+	pipenv requirements > requirements.txt
+
 compile:
 	pipenv run cython -2 ccc/cl.pyx
 build:
diff --git a/requirements.txt b/requirements.txt
index 6e71305..9f1bb9e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,77 +1,13 @@
 -i https://pypi.org/simple
-alabaster==0.7.13 ; python_version >= '3.6'
-astroid==2.11.7 ; python_full_version >= '3.6.2'
-attrs==22.2.0 ; python_version >= '3.6'
-babel==2.11.0 ; python_version >= '3.6'
-bleach==6.0.0 ; python_version >= '3.7'
-certifi==2022.12.7 ; python_version >= '3.6'
-cffi==1.15.1
-charset-normalizer==3.0.1 ; python_version >= '3.6'
-colorama==0.4.6 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
-coverage[toml]==7.1.0 ; python_version >= '3.7'
-cryptography==39.0.1 ; python_version >= '3.6'
-cython==0.29.30
-dill==0.3.6 ; python_version >= '3.7'
-docutils==0.18.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-enthought-sphinx-theme==0.7.1
-exceptiongroup==1.1.0
-idna==3.4 ; python_version >= '3.5'
-imagesize==1.4.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-importlib-metadata==6.0.0 ; python_version < '3.10'
-iniconfig==2.0.0 ; python_version >= '3.7'
-isort==5.11.4 ; python_full_version >= '3.7.0'
-jaraco.classes==3.2.3 ; python_version >= '3.7'
-jeepney==0.8.0 ; sys_platform == 'linux'
-jinja2==3.1.2 ; python_version >= '3.7'
-keyring==23.13.1 ; python_version >= '3.7'
-lazy-object-proxy==1.9.0 ; python_version >= '3.7'
-markupsafe==2.1.2 ; python_version >= '3.7'
-mccabe==0.7.0 ; python_version >= '3.6'
-more-itertools==9.0.0 ; python_version >= '3.7'
-packaging==23.0 ; python_version >= '3.7'
-pkginfo==1.9.6 ; python_version >= '3.6'
-platformdirs==2.6.2 ; python_version >= '3.7'
-pluggy==1.0.0 ; python_version >= '3.6'
-py-cpuinfo==9.0.0
-pycparser==2.21
-pygments==2.14.0 ; python_version >= '3.6'
-pylint==2.13.9
-pyperclip==1.8.2
-pytest==7.2.0
-pytest-benchmark==4.0.0
-pytest-cov==3.0.0
-pytz==2022.7.1
-readme-renderer==37.3 ; python_version >= '3.7'
-requests==2.28.2 ; python_version >= '3.7' and python_version < '4'
-requests-toolbelt==0.10.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-rfc3986==2.0.0 ; python_version >= '3.7'
-secretstorage==3.3.3 ; sys_platform == 'linux'
-setuptools==65.5.1
-six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-snowballstemmer==2.2.0
-sphinx==5.0.0
-sphinxcontrib-applehelp==1.0.4 ; python_version >= '3.8'
-sphinxcontrib-devhelp==1.0.2 ; python_version >= '3.5'
-sphinxcontrib-htmlhelp==2.0.0 ; python_version >= '3.6'
-sphinxcontrib-jsmath==1.0.1 ; python_version >= '3.5'
-sphinxcontrib-qthelp==1.0.3 ; python_version >= '3.5'
-sphinxcontrib-serializinghtml==1.1.5 ; python_version >= '3.5'
-tabulate==0.8.9
-tomli==2.0.1 ; python_version < '3.11'
-tqdm==4.64.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-twine==3.7.1
-typing-extensions==4.4.0 ; python_version < '3.10'
-urllib3==1.26.14 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
-webencodings==0.5.1
-wrapt==1.14.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-zipp==3.11.0 ; python_version >= '3.7'
 association-measures==0.2.6
-bottleneck==1.3.6
-numexpr==2.8.4
-numpy==1.24.1 ; python_version >= '3.8'
+bottleneck==1.3.7
+numexpr==2.8.5
+numpy==1.25.2 ; python_version >= '3.9'
 pandas==1.5.3
 python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
-pyyaml==6.0
-scipy==1.10.0 ; python_version < '3.12' and python_version >= '3.8'
+pytz==2023.3
+pyyaml==6.0.1
+scipy==1.11.1 ; python_version < '3.13' and python_version >= '3.9'
+six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
 unidecode==1.3.6
-wheel==0.38.4
+wheel==0.41.1
diff --git a/scripts/compare-arg-results.py b/scripts/compare-arg-results.py
index b7a1b46..0d1186d 100644
--- a/scripts/compare-arg-results.py
+++ b/scripts/compare-arg-results.py
@@ -1,9 +1,9 @@
-import json
 import gzip
+import json
 from glob import glob
-from pandas import read_csv, DataFrame
 
 from cutils.times import Progress
+from pandas import DataFrame, read_csv
 
 
 def get_meta(path_meta):
diff --git a/scripts/freqs2keywords.py b/scripts/freqs2keywords.py
index efd1cf6..3e7e56d 100644
--- a/scripts/freqs2keywords.py
+++ b/scripts/freqs2keywords.py
@@ -1,7 +1,8 @@
-from pandas import read_csv, to_numeric
-from association_measures import measures, frequencies
 from argparse import ArgumentParser
 
+from association_measures import frequencies, measures
+from pandas import read_csv, to_numeric
+
 
 def calculate_keywords(df1, df2, C1, C2, lonely=True, how='first'):
 
diff --git a/scripts/list2freq.py b/scripts/list2freq.py
index 04d4057..0a7569a 100644
--- a/scripts/list2freq.py
+++ b/scripts/list2freq.py
@@ -1,6 +1,7 @@
-from pandas import read_csv, DataFrame
 from argparse import ArgumentParser
 
+from pandas import DataFrame, read_csv
+
 
 def main(path_in, path_out, col):
     print('reading')
diff --git a/scripts/vrt2index-script.py b/scripts/vrt2index-script.py
index e009423..394ea4e 100644
--- a/scripts/vrt2index-script.py
+++ b/scripts/vrt2index-script.py
@@ -1,9 +1,10 @@
 import gzip
 import os
+from argparse import ArgumentParser
+from collections import defaultdict
+
 from cutils.times import Progress
 from cutils.vrt import meta2dict
-from collections import defaultdict
-from argparse import ArgumentParser
 
 
 def create_file(path_in, corpus_name, registry_dir, registry_name, data, p_att, s_att):
diff --git a/setup.py b/setup.py
index cef3f7a..a5a6a8f 100755
--- a/setup.py
+++ b/setup.py
@@ -93,15 +93,15 @@
         'ccc'
     ],
     ext_modules=extensions,
-    python_requires='>=3.6.2',
+    python_requires='>=3.7.2',
     install_requires=[
-        "wheel>=0.37.1",
-        "association-measures>=0.2.6",
-        "pandas>=1.1.5",
-        "numexpr>=2.7.1",
-        "Bottleneck>=1.3.4",
-        "unidecode>=1.3.4",
-        "pyyaml>=6.0"
+        "wheel>=0.41.1,<0.42",
+        "association-measures>=0.2.6,<0.3",
+        "pandas>=1.3.5,<2.0",
+        "numexpr>=2.8.5,<3.0",
+        "Bottleneck>=1.3.7,<2.0",
+        "unidecode>=1.3.6,<2.0",
+        "pyyaml>=6.0.1,<7.0"
     ],
     classifiers=[
         "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
@@ -112,6 +112,6 @@
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
-        'Programming Language :: Cython',
+        'Programming Language :: Cython'
     ],
 )


Test patch:
diff --git a/pytest.ini b/pytest.ini
index 9385117..3b952bc 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,6 +1,6 @@
 [pytest]
-# log_cli=true
-# log_level=INFO
+log_cli=true
+log_level=INFO
 log_format = [%(asctime)s] %(name)s %(levelname)s %(message)s
 log_date_format = %Y-%m-%d %H:%M:%S
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 52b9c26..f6cba21 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -41,8 +41,8 @@ def germaparl():
     """ settings for small germaparl testing corpus """
 
     corpus_name = 'GERMAPARL1386'
-    registry_path = os.path.join(DIR_PATH, "corpora", "registry")
-    lib_path = os.path.join(DIR_PATH, "corpora", "library")
+    registry_dir = os.path.join(DIR_PATH, "corpora", "registry")
+    lib_dir = os.path.join(DIR_PATH, "corpora", "library")
     freq_list = os.path.join(DIR_PATH, "corpora", "counts",
                              "germaparl1386-lemma-freq.tsv")
     dump_path = os.path.join(DIR_PATH, "corpora", "counts",
@@ -65,7 +65,7 @@ def germaparl():
     query_lib = '/np[] [lemma="zeigen"]'
 
     return {
-        'registry_path': registry_path,
+        'registry_dir': registry_dir,
         'corpus_name': corpus_name,
         'context': context,
         's_context': s_context,
@@ -77,7 +77,7 @@ def germaparl():
         'query_full': query_within,
         'dump': seehofer_dump,
         'freq_list': freq_list,
-        'lib_path': lib_path,
+        'lib_dir': lib_dir,
         'query_lib': query_lib
     }
 
@@ -87,8 +87,8 @@ def germaparl_full():
     """ settings for local germaparl corpus """
 
     corpus_name = "GERMAPARL-1949-2021"
-    registry_path = "/usr/local/share/cwb/registry"
-    lib_path = None
+    registry_dir = "/usr/local/share/cwb/registry"
+    lib_dir = None
 
     context = 50
     s_context = 'speaker_node'
@@ -96,12 +96,12 @@ def germaparl_full():
     s_query = 's'
 
     return {
-        'registry_path': registry_path,
+        'registry_dir': registry_dir,
         'corpus_name': corpus_name,
         'context': context,
         's_context': s_context,
         's_query': s_query,
-        'lib_path': lib_path,
+        'lib_dir': lib_dir,
     }
 
 
diff --git a/tests/local-test.py b/tests/local-test.py
index 63fa0af..9d7eab4 100644
--- a/tests/local-test.py
+++ b/tests/local-test.py
@@ -1,7 +1,10 @@
+import pytest
 from pandas import concat, read_csv
 
 from ccc import Corpus
 from ccc.discoursemes import create_constellation
+from ccc.dumps import Dumps
+from ccc.keywords import keywords
 
 from .conftest import DATA_PATH
 
@@ -149,7 +152,7 @@ def test_constellation_concordance():
                                  flags,
                                  escape,
                                  # CWB setttings
-                                 data_path=DATA_PATH,
+                                 data_dir=DATA_PATH,
                                  approximate=True)
 
     # retrieve lines
@@ -167,3 +170,79 @@ def test_constellation_concordance():
         cut_off=cut_off
     )
     print(collocates)
+
+
+def test_dumps_collocates_slow():
+
+    # subcorpora via s-attribute values
+    parties = {
+        # 'green': {"GRUENE", "Bündnis 90/Die Grünen"},
+        'red': {'SPD'},
+        # 'black': {'CDU', 'CSU'},
+        'yellow': {'FDP'},
+        # 'purple': {'PDS'}
+    }
+
+    # collocates
+    corpus = Corpus("GERMAPARL-1949-2021")
+    dumps = Dumps(corpus, parties, s_att='parliamentary_group')
+    tables = dumps.collocates(
+        cqp_query='"Atomkraft"',
+        order='log_ratio',
+        context_break='s',
+        window=20
+    )
+    assert len(tables) == len(parties)
+    # assert tables['yellow'].index[0] == 'Grad'
+
+
+def test_keywords():
+    germaparl = Corpus("GERMAPARL-1949-2021")
+    sz = Corpus("SZ-2009-2014")
+    print(keywords(germaparl, sz, cut_off=None))
+
+
+def test_keywords_NA():
+
+    corpus = Corpus("GERMAPARL-1949-2021")
+    parties = {
+        'green': {"GRUENE", "Bündnis 90/Die Grünen"},
+        'red': {'SPD'},
+        'black': {'CDU', 'CSU'},
+        'yellow': {'FDP'},
+        'purple': {'PDS'}
+    }
+    factions = Dumps(corpus, s_dict=parties, s_att='parliamentary_group')
+    collocates = factions.collocates(cqp_query='[lemma="Klimawandel"]', cut_off=None)
+    print(collocates)
+
+
+@pytest.mark.now
+def test_associations_slow():
+
+    from collections import defaultdict
+
+    from pandas import read_csv
+
+    discoursemes = read_csv("/home/ausgerechnet/repositories/thesis/discoursemes/climate-change.tsv",
+                            sep="\t")
+    cc_collocates = read_csv("/home/ausgerechnet/repositories/thesis/discoursemes/cc-collocates-clr50.tsv",
+                             sep="\t")
+    cc_discoursemes = defaultdict(list)
+    cc_discoursemes['climate change'] = discoursemes.loc[discoursemes['name'] == 'Klimawandel']['query'].to_list()
+
+    for d in cc_collocates.to_dict('records'):
+        cc_discoursemes[d['class']].append(d['item'])
+
+    const = create_constellation(
+        corpus_name='GERMAPARL-1949-2021',
+        topic_discourseme={},
+        filter_discoursemes={},
+        additional_discoursemes=cc_discoursemes,
+        s_context='s',
+        p_query='lemma',
+        s_query='s',
+        escape=False
+    )
+
+    const.associations()
diff --git a/tests/test_01_utils.py b/tests/test_01_utils.py
index c884d9a..83c6e1f 100644
--- a/tests/test_01_utils.py
+++ b/tests/test_01_utils.py
@@ -1,5 +1,6 @@
 from ccc import Corpus
-from ccc.utils import filter_df, fold_item, merge_intervals, preprocess_query
+from ccc.utils import (filter_df, fold_item, intersect_intervals,
+                       merge_intervals, preprocess_query)
 
 from .conftest import DATA_PATH
 
@@ -73,16 +74,34 @@ def test_fold_items():
     )
 
 
+def test_filter_df(germaparl):
+    c = Corpus(germaparl['corpus_name'], registry_dir=germaparl['registry_dir'], data_dir=DATA_PATH)
+    subcorpus = c.query(germaparl['query'])
+    coll = subcorpus.collocates()
+    assert ',' in coll.index
+    coll_filtered = filter_df(coll, 'resources/stopwords-de.txt')
+    assert ',' not in coll_filtered.index
+
+
 def test_merge_intervals():
+
     intervals = [[1, 3], [2, 4], [5, 9], [6, 10]]
     merge = merge_intervals(intervals)
-    assert(merge == [[1, 4], [5, 10]])
+    assert merge == [[1, 4], [5, 10]]
 
+    intervals = [[0, 5], [2, 4], [6, 9], [6, 10]]
+    merge = merge_intervals(intervals)
+    assert merge == [[0, 5], [6, 10]]
 
-def test_filter_df(germaparl):
-    c = Corpus(germaparl['corpus_name'], registry_path=germaparl['registry_path'], data_path=DATA_PATH)
-    dump = c.query(germaparl['query'])
-    coll = dump.collocates()
-    assert ',' in coll.index
-    coll_filtered = filter_df(coll, 'resources/stopwords-de.txt')
-    assert ',' not in coll_filtered.index
+
+def test_intersect_intervals():
+
+    intervals_left = [[0, 1801], [7438, 7449]]
+    intervals_right = [[458, 463], [548, 553], [1025, 1500], [2540, 2670], [7438, 7439], [7440, 7449], [8500, 8600]]
+    merge = intersect_intervals(intervals_left, intervals_right)
+    assert merge == [[458, 463], [548, 553], [1025, 1500], [7438, 7439], [7440, 7449]]
+
+    intervals_left = [[0, 1801], [7438, 7449]]
+    intervals_right = [[458, 463], [548, 553], [1025, 1500], [1800, 1802], [7438, 7439], [7440, 7449], [8500, 8600]]
+    merge = intersect_intervals(intervals_left, intervals_right)
+    assert merge == [[458, 463], [548, 553], [1025, 1500], [1800, 1801], [7438, 7439], [7440, 7449]]
diff --git a/tests/test_02_cl_cqp.py b/tests/test_02_cl_cqp.py
index 2ecf6a2..77242ef 100644
--- a/tests/test_02_cl_cqp.py
+++ b/tests/test_02_cl_cqp.py
@@ -36,7 +36,7 @@ def test_cqp_kill():
 def test_cqp_query(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
     cqp.Query('"Horst"')
@@ -46,7 +46,7 @@ def test_cqp_query(germaparl):
 def test_cqp_dump(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
     cqp.Query('"Horst"')
@@ -59,7 +59,7 @@ def test_cqp_dump(germaparl):
 def test_cqp_undump(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
     cqp.Query('"Horst"')
@@ -73,7 +73,7 @@ def test_cqp_undump(germaparl):
 def test_cqp_group(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
     cqp.Query('[lemma="Horst"] [lemma="Seehofer"]')
@@ -86,7 +86,7 @@ def test_cqp_group(germaparl):
 def test_nqr_from_query(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
     assert(int(cqp.Exec('size Seehofer;')) == 0)
@@ -102,7 +102,7 @@ def test_nqr_from_query(germaparl):
 def test_nqr_from_dump(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
     assert(int(cqp.Exec('size Seehof;')) == 0)
@@ -114,7 +114,7 @@ def test_nqr_from_dump(germaparl):
 
 def test_cl(germaparl):
     corpus = Corpus(germaparl['corpus_name'],
-                    registry_dir=germaparl['registry_path'])
+                    registry_dir=germaparl['registry_dir'])
 
     # get sentences, words, pos-tags
     sentences = corpus.attribute('s', 's')
@@ -136,7 +136,7 @@ def test_cl(germaparl):
 def test_nqr_from_dump_error(germaparl):
     cqp = CQP(
         binary="cqp",
-        options='-c -r ' + germaparl['registry_path']
+        options='-c -r ' + germaparl['registry_dir']
     )
     cqp.Exec(germaparl['corpus_name'])
 
diff --git a/tests/test_03_cache.py b/tests/test_03_cache.py
index 156d576..c845cf2 100644
--- a/tests/test_03_cache.py
+++ b/tests/test_03_cache.py
@@ -21,7 +21,7 @@ def test_set_get_empty():
 def test_set_get():
 
     cache = Cache(os.path.join(DATA_PATH, 'test-cache'))
-    parameters = {'query': "test", 's_break': "test", 'key': randint(0, 9e+9)}
+    parameters = {'query': "test", 's_break': "test", 'key': randint(0, 9 * 10^9)}
     dump = DataFrame()
     r = cache.get(parameters.values())
     assert(r is None)
@@ -36,4 +36,4 @@ def test_set_get():
 
 
 def test_generate_library_idx(germaparl):
-    assert isinstance(generate_library_idx(germaparl['lib_path']), str)
+    assert isinstance(generate_library_idx(germaparl['lib_dir']), str)
diff --git a/tests/test_04_cwb.py b/tests/test_04_cwb.py
index ae4ef9c..256aefa 100644
--- a/tests/test_04_cwb.py
+++ b/tests/test_04_cwb.py
@@ -3,23 +3,23 @@
 import pandas as pd
 import pytest
 
-from ccc import Corpora, Corpus
+from ccc import Corpora, Corpus, SubCorpus
 
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH, lib=True):
+def get_corpus(corpus_settings, data_dir=DATA_PATH, lib=True):
 
     if lib:
-        lib_path = corpus_settings.get('lib_path', None)
+        lib_dir = corpus_settings.get('lib_dir', None)
     else:
-        lib_path = None
+        lib_dir = None
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=lib_path,
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=lib_dir,
+        data_dir=data_dir
     )
 
 
@@ -29,7 +29,7 @@ def get_corpus(corpus_settings, data_path=DATA_PATH, lib=True):
 
 @pytest.mark.init
 def test_corpora(germaparl):
-    corpora = Corpora(registry_path=germaparl['registry_path'])
+    corpora = Corpora(registry_dir=germaparl['registry_dir'])
     assert type(corpora.show()) == pd.DataFrame
     assert "GERMAPARL1386" in corpora.show().index
 
@@ -49,12 +49,12 @@ def test_get_corpus(germaparl):
 @pytest.mark.init
 def test_corpus_descriptor(germaparl):
     corpus = get_corpus(germaparl)
-    assert isinstance(corpus.attributes_available, pd.DataFrame)
+    assert isinstance(corpus.available_attributes(), pd.DataFrame)
 
 
 @pytest.mark.init
 def test_data_dir(germaparl):
-    get_corpus(germaparl, lib=False, data_path=None)
+    get_corpus(germaparl, lib=False, data_dir=None)
     paths = glob("/tmp/ccc-*")
     assert len(paths) >= 1
 
@@ -65,15 +65,15 @@ def test_data_dir(germaparl):
 @pytest.mark.init
 def test_macros(germaparl):
     corpus = get_corpus(germaparl, lib=False)
-    assert '/np(0)' not in corpus._macros_available()
+    assert '/np(0)' not in corpus.available_macros()
     corpus = get_corpus(germaparl, lib=True)
-    assert '/np(0)' in corpus._macros_available()
+    assert '/np(0)' in corpus.available_macros()
 
 
 @pytest.mark.init
 def test_wordlists(germaparl):
     corpus = get_corpus(germaparl, lib=True)
-    assert "$parties" in corpus._wordlists_available()
+    assert "$parties" in corpus.available_wordlists()
 
 
 #####################################################
@@ -125,7 +125,7 @@ def test_marginals_pattern(germaparl):
 @pytest.mark.marginals
 def test_marginals_complex(germaparl):
     corpus = get_corpus(germaparl)
-    df = corpus.marginals_complex(
+    df = corpus._marginals_complex(
         [("gehen", "VVFIN"), ("Seehofer", "NE"), ("Merkel", "NE")],
         ["lemma", "pos"]
     )
@@ -133,94 +133,6 @@ def test_marginals_complex(germaparl):
     assert df['freq']['gehen VVFIN'] == 186
 
 
-#####################################################
-# SUBCORPORA ########################################
-#####################################################
-
-@pytest.mark.subcorpus
-def test_activate_subcorpus(germaparl):
-
-    corpus = get_corpus(germaparl)
-
-    df1 = corpus.dump_from_query(
-        "[lemma='Seehofer']"
-    )
-    corpus.query(
-        "[lemma='Horst'] expand to s",
-        name='Horst'
-    ).df
-    horst = corpus.activate_subcorpus('Horst')
-    df2 = horst.dump_from_query(
-        "[lemma='Seehofer']"
-    )
-    assert len(df1) > len(df2)
-
-
-@pytest.mark.subcorpus
-def test_deactivate_subcorpus(germaparl):
-
-    corpus = get_corpus(germaparl)
-
-    df1 = corpus.dump_from_query(
-        '[lemma="die"]',
-        germaparl['s_query']
-    )
-
-    # define subcorpus
-    corpus.query(
-        cqp_query="[lemma='sein'] expand to s",
-        name='Sein'
-    )
-
-    # activate subcorpus
-    sein = corpus.activate_subcorpus('Sein')
-    df2 = sein.dump_from_query(
-        '[lemma="die"]',
-        germaparl['s_query']
-    )
-
-    # deactivate subcorpus
-    corpus.activate_subcorpus()
-    df3 = corpus.dump_from_query(
-        '[lemma="die"]',
-        germaparl['s_query']
-    )
-
-    # deactivate subcorpus
-    corpus.activate_subcorpus()
-    df4 = corpus.dump_from_query(
-        '[lemma="die"]',
-        germaparl['s_query']
-    )
-
-    assert len(df1) == len(df3) == len(df4)
-    assert len(df1) > len(df2)
-
-
-@pytest.mark.subcorpus
-def test_create_cached_nqr(germaparl):
-
-    # problem: if a query runs once without having been given a name,
-    # the CQP dump (NQR) is not saved to disk
-    # if the same query runs again *with a name*, it should be saved to disk
-
-    corpus = get_corpus(germaparl)
-    corpus.query('[lemma="jetzt"]')
-    # assert "Jetzt" not in corpus.show_nqr().values
-    corpus.query('[lemma="jetzt"]', name='Jetzt')
-    assert "Jetzt" in corpus.show_nqr().values
-    corpus.activate_subcorpus("Jetzt")
-
-
-@pytest.mark.subcorpus
-def test_nqr_from_s_att(germaparl):
-
-    corpus = get_corpus(germaparl)
-    corpus.query_s_att("text_party", values={"CDU", "CSU"}, name="Union")
-    assert "Union" in corpus.show_nqr().values
-    corpus.activate_subcorpus("Union")
-
-
 ################################################
 # CREATING DUMPS ###############################
 ################################################
@@ -556,3 +468,172 @@ def test_ccc_quick_query_2(germaparl):
     )
 
     assert identifier in corpus.show_nqr()['subcorpus'].values
+
+
+#####################################################
+# SUBCORPORA ########################################
+#####################################################
+
+@pytest.mark.subcorpus
+def test_activate_subcorpus(germaparl):
+
+    corpus = get_corpus(germaparl)
+
+    df1 = corpus.dump_from_query(
+        "[lemma='Seehofer']"
+    )
+    corpus.query(
+        "[lemma='Horst'] expand to s",
+        name='Horst'
+    ).df
+
+    horst = corpus.subcorpus('Horst')
+    df2 = horst.dump_from_query(
+        "[lemma='Seehofer']"
+    )
+    assert len(df1) > len(df2)
+
+
+@pytest.mark.subcorpus
+def test_deactivate_subcorpus(germaparl):
+
+    corpus = get_corpus(germaparl)
+    # query corpus
+    df1 = corpus.dump_from_query(
+        '[lemma="die"]',
+        germaparl['s_query']
+    )
+
+    # query subcorpus
+    sein = corpus.query(
+        cqp_query="[lemma='sein'] expand to s",
+        name='Sein'
+    )
+    df2 = sein.dump_from_query(
+        '[lemma="die"]',
+        germaparl['s_query']
+    )
+
+    # query corpus
+    df3 = corpus.dump_from_query(
+        '[lemma="die"]',
+        germaparl['s_query']
+    )
+
+    assert len(df1) == len(df3)
+    assert len(df1) > len(df2)
+
+
+@pytest.mark.subcorpus
+def test_create_cached_nqr(germaparl):
+
+    # problem: if a query runs once without having been given a name,
+    # the CQP dump (NQR) is not saved to disk
+    # if the same query runs again *with a name*, it should be saved to disk
+
+    corpus = get_corpus(germaparl)
+
+    subcorpus = corpus.query('[lemma="jetzt"]')
+    assert isinstance(subcorpus, SubCorpus)
+    # assert "Jetzt" not in corpus.show_nqr().values
+
+    subcorpus = corpus.query('[lemma="jetzt"]', name='Jetzt')
+    assert isinstance(subcorpus, SubCorpus)
+    # assert "Jetzt" in corpus.show_nqr().values
+
+
+@pytest.mark.subcorpus
+def test_nqr_from_s_att(germaparl):
+
+    corpus = get_corpus(germaparl)
+    corpus.query_s_att("text_party", values={"CDU", "CSU"}, name="Union")
+    assert "Union" in corpus.show_nqr().values
+    assert isinstance(corpus.subcorpus("Union"), SubCorpus)
+
+
+@pytest.mark.subcorpus
+def test_subcorpus(germaparl):
+
+    corpus = get_corpus(germaparl)
+    subcorpus = corpus.query(
+        cqp_query='[lemma="und"]',
+        context_break=germaparl['s_query'],
+        name='Test'
+    )
+
+    # init
+    assert isinstance(subcorpus, SubCorpus)
+    assert len(subcorpus.df) == 2880
+    assert all(elem in subcorpus.df.columns for elem in
+               ["s_cwbid", "s_span", "s_spanend", "contextid", "context", "contextend"])
+
+
+@pytest.mark.subcorpus
+def test_subcorpus_keywords(germaparl):
+
+    corpus = get_corpus(germaparl)
+    subcorpus = corpus.query(
+        cqp_query='[lemma="und"]',
+        context_break=germaparl['s_query'],
+        name='Test'
+    )
+
+    # keywords
+    assert len(subcorpus.keywords()) == 1
+    assert "und" in subcorpus.keywords().index
+
+
+@pytest.mark.subcorpus
+def test_subcorpus_collocates(germaparl):
+
+    corpus = get_corpus(germaparl)
+    subcorpus = corpus.query(
+        cqp_query='[lemma="und"]',
+        context_break=germaparl['s_query'],
+        name='Test'
+    )
+    assert len(subcorpus.collocates(window=1)) == 100
+    assert len(subcorpus.collocates(window=10)) == 100
+
+
+@pytest.mark.subcorpus
+def test_subcorpus_context(germaparl):
+
+    corpus = get_corpus(germaparl)
+    subcorpus = corpus.query(
+        cqp_query='[lemma="und"]',
+        context_break=germaparl['s_query'],
+        name='Test'
+    )
+    assert len(subcorpus.collocates(window=10)) == 100
+    assert len(subcorpus.collocates(window=20)) == 100
+    # collocates
+    subcorpus = subcorpus.set_context(20)
+    assert len(subcorpus.collocates()) == 100
+    assert len(subcorpus.collocates(window=10)) == 100
+
+
+@pytest.mark.subcorpus
+def test_subcorpus_marginals(germaparl):
+
+    corpus = get_corpus(germaparl)
+    assert len(corpus.marginals()) == 14034
+    subcorpus = corpus.query(
+        cqp_query='[lemma="und"]',
+        context_break=germaparl['s_query'],
+        name='Test'
+    )
+    assert len(subcorpus.marginals()) == 2
+
+
+@pytest.mark.subcorpus
+def test_subcorpus_query_s_att(germaparl):
+
+    corpus = get_corpus(germaparl)
+    black = corpus.query_s_att("text_party", values={"CDU", "CSU"}, name="Union")
+    assert "Union" in corpus.show_nqr().values
+    assert isinstance(black, SubCorpus)
+
+    interjection = corpus.query_s_att("p_type", values={"interjection"})
+    black_interjection = black.query_s_att("p_type", values={"interjection"})
+    assert len(black.matches()) > len(interjection.matches()) > len(black_interjection.matches())
diff --git a/tests/test_05_concordance.py b/tests/test_05_concordance.py
index 4a6e60d..1f92553 100644
--- a/tests/test_05_concordance.py
+++ b/tests/test_05_concordance.py
@@ -7,13 +7,13 @@
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
diff --git a/tests/test_06_cqpy.py b/tests/test_06_cqpy.py
index 5dac2c6..eed1899 100644
--- a/tests/test_06_cqpy.py
+++ b/tests/test_06_cqpy.py
@@ -6,13 +6,13 @@
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
diff --git a/tests/test_07_counts.py b/tests/test_07_counts.py
index b6a01b7..0f99491 100644
--- a/tests/test_07_counts.py
+++ b/tests/test_07_counts.py
@@ -11,13 +11,13 @@
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
@@ -27,7 +27,7 @@ def test_lexdecode_and_read(germaparl):
     df1, R1 = read_freq_list(germaparl['freq_list'],
                              columns=['lemma'])
     df2, R2 = cwb_lexdecode(germaparl['corpus_name'],
-                            germaparl['registry_path'],
+                            germaparl['registry_dir'],
                             'lemma')
 
     assert(R1 == R2)
@@ -47,7 +47,7 @@ def test_cwb_scan_corpus_subcorpora(germaparl):
     with NamedTemporaryFile(mode="wt") as f:
         cqp.Exec('dump Tmp > "%s"' % f.name)
         df1, R1 = cwb_scan_corpus(germaparl['corpus_name'],
-                                  germaparl['registry_path'],
+                                  germaparl['registry_dir'],
                                   f.name)
 
     # activate a sub-corpus
@@ -61,7 +61,7 @@ def test_cwb_scan_corpus_subcorpora(germaparl):
     with NamedTemporaryFile(mode="wt") as f:
         cqp.Exec('dump Tmp > "%s"' % f.name)
         df2, R2 = cwb_scan_corpus(germaparl['corpus_name'],
-                                  germaparl['registry_path'],
+                                  germaparl['registry_dir'],
                                   f.name)
 
     # check that results are different
@@ -76,11 +76,11 @@ def test_cwb_scan_corpus_marginal(germaparl):
 
     df1, R1 = cwb_scan_corpus(
         germaparl['corpus_name'],
-        germaparl['registry_path']
+        germaparl['registry_dir']
     )
     df2, R2 = cwb_scan_corpus(
         germaparl['corpus_name'],
-        germaparl['registry_path'],
+        germaparl['registry_dir'],
         p_atts=['lemma', 'pos']
     )
     assert(df1.index.name == 'item')
diff --git a/tests/test_08_collocates.py b/tests/test_08_collocates.py
index 8559f3f..b16fb1e 100644
--- a/tests/test_08_collocates.py
+++ b/tests/test_08_collocates.py
@@ -8,17 +8,18 @@
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
 @pytest.mark.default
+@pytest.mark.now
 def test_collo_single(germaparl):
     corpus = get_corpus(germaparl)
     query = ('[word="\\("] [lemma=".*"]+ [word="\\)"]')
diff --git a/tests/test_09_keywords.py b/tests/test_09_keywords.py
index 5259f4d..bab0098 100644
--- a/tests/test_09_keywords.py
+++ b/tests/test_09_keywords.py
@@ -7,13 +7,13 @@
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
@@ -125,3 +125,34 @@ def test_keywords_func_corpora(germaparl):
     corpus = get_corpus(germaparl)
     kw = keywords(corpus, corpus, ['lemma'], ['word'], 'conservative_log_ratio')
     assert kw.index[0] == 'sie'
+
+
+@pytest.mark.subcorpus
+def test_keywords_func_corpora_p_atts(germaparl):
+
+    corpus = get_corpus(germaparl)
+    kw = keywords(corpus, corpus, ['lemma', 'pos'], ['lemma', 'pos'], 'conservative_log_ratio')
+
+    assert kw.index[0] == '! $.'
+
+
+@pytest.mark.subcorpus
+def test_keywords_func_p_atts(germaparl):
+
+    corpus = get_corpus(germaparl)
+    green = corpus.query_s_att("text_party", {"GRUENE", "Bündnis 90/Die Grünen"})
+    red = corpus.query_s_att("text_party", {"SPD", "S.P.D."})
+    kw = keywords(green, red, ['lemma', 'pos'], ['lemma', 'pos'], 'conservative_log_ratio')
+
+    assert kw.index[0] == 'Dr. NN'
+
+
+@pytest.mark.subcorpus
+def test_keywords_func_p_atts_2(germaparl):
+
+    corpus = get_corpus(germaparl)
+    green = corpus.query_s_att("text_party", {"GRUENE", "Bündnis 90/Die Grünen"})
+    red = corpus.query_s_att("text_party", {"SPD", "S.P.D."})
+    kw = keywords(green, red, ['word', 'pos'], ['lemma', 'pos'], 'conservative_log_ratio')
+
+    assert kw.index[0] == 'der ART'
diff --git a/tests/test_10_dumps.py b/tests/test_10_dumps.py
index f5d3b4b..b2249bf 100644
--- a/tests/test_10_dumps.py
+++ b/tests/test_10_dumps.py
@@ -7,13 +7,13 @@
 from .conftest import DATA_PATH
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
@@ -22,17 +22,17 @@ def get_corpus(corpus_settings, data_path=DATA_PATH):
 ########
 def test_query(germaparl):
     corpus = get_corpus(germaparl)
-    dump = corpus.query('"SPD"')
-    assert dump.name_cqp == 'Last'
-    assert dump.size == 632
-    assert (dump.df.columns == ['context', 'contextend']).all()
+    subcorpus = corpus.query('"SPD"')
+    assert subcorpus.subcorpus_name == 'Last'
+    assert len(subcorpus.df) == 632
+    assert (subcorpus.df.columns == ['context', 'contextend']).all()
 
 
 def test_query_name(germaparl):
     corpus = get_corpus(germaparl)
     dump = corpus.query('"SPD"', name="SPD")
-    assert dump.name_cqp == "SPD"
-    assert dump.size == 632
+    assert dump.subcorpus_name == "SPD"
+    assert len(dump.df) == 632
     assert (dump.df.columns == ['context', 'contextend']).all()
 
 
@@ -67,8 +67,7 @@ def test_matches_subcorpus(germaparl):
     corpus = get_corpus(germaparl)
     dump_base = corpus.query(r'[pos="NE"]? [pos="NE"] "\[" ".*" "\]"', name="Base")
     tokens_base = len(dump_base.matches())
-    corpus.subcorpus = "Base"
-    dump_neg = corpus.query('[pos="NE"]')
+    dump_neg = dump_base.query('[pos="NE"]')
     tokens_neg = len(dump_neg.matches())
     assert (tokens_base - tokens_neg) == 350
 
@@ -94,8 +93,8 @@ def test_query_s_satt(germaparl):
     corpus = get_corpus(germaparl)
     parties = {"GRUENE", "Bündnis 90/Die Grünen"}
     dump = corpus.query_s_att('text_party', parties)
-    assert dump.name_cqp is None
-    assert dump.size == 87
+    assert dump.subcorpus_name is not None
+    assert len(dump.df) == 87
     assert (dump.df.columns == ['text_party_cwbid', 'text_party']).all()
 
 
@@ -181,12 +180,12 @@ def test_dumps_keywords(germaparl):
     # keywords
     corpus = get_corpus(germaparl)
     dumps = Dumps(corpus, parties, s_att='text_party')
-    tables = dumps.keywords(order='log_ratio')
-    assert tables['green'].index[0] == "Oppositionsfraktion"
-    assert tables['red'].index[0] == "Bereicherung"
-    assert tables['black'].index[0] == "Abgabenquote"
-    assert tables['yellow'].index[0] == "Wirtschafts-"
-    assert tables['purple'].index[0] == "ÖPNV-Gesetz"
+    table = dumps.keywords(order='log_ratio')
+    assert table.loc['green'].index[0] == "Oppositionsfraktion"
+    assert table.loc['red'].index[0] == "Bereicherung"
+    assert table.loc['black'].index[0] == "Abgabenquote"
+    assert table.loc['yellow'].index[0] == "Wirtschafts-"
+    assert table.loc['purple'].index[0] == "ÖPNV-Gesetz"
 
 
 @pytest.mark.dumps
@@ -204,16 +203,17 @@ def test_dumps_collocates(germaparl):
     # collocates
     corpus = get_corpus(germaparl)
     dumps = Dumps(corpus, parties, s_att='text_party')
-    tables = dumps.collocates(
+    table = dumps.collocates(
         cqp_query='"Wirtschaft"',
         order='log_ratio',
         context_break='s',
         window=20
     )
-    assert len(tables) == len(parties)
-    assert tables['yellow'].index[0] == 'Grad'
+    assert len(set(table.index.get_level_values('subcorpus'))) == len(parties)
+    assert table.loc['yellow'].index[0] == 'Grad'
 
 
+@pytest.mark.dumps
 def test_dumps_collocates_global(germaparl):
 
     # subcorpora via s-attribute values
@@ -228,15 +228,15 @@ def test_dumps_collocates_global(germaparl):
     # collocates
     corpus = get_corpus(germaparl)
     dumps = Dumps(corpus, parties, s_att='text_party')
-    tables = dumps.collocates(
+    table = dumps.collocates(
         cqp_query='"Wirtschaft"',
         order='log_ratio',
         context_break='s',
         window=20,
-        reference='global'
+        marginals='corpus'
     )
-    assert len(tables) == len(parties)
-    assert tables['yellow'].index[0] == 'Grad'
+    assert len(set(table.index.get_level_values('subcorpus'))) == len(parties)
+    assert table.loc['yellow'].index[0] == 'Grad'
 
 
 @pytest.mark.benchmark
diff --git a/tests/test_11_discoursemes.py b/tests/test_11_discoursemes.py
index af02646..e334711 100644
--- a/tests/test_11_discoursemes.py
+++ b/tests/test_11_discoursemes.py
@@ -40,13 +40,13 @@
 #   get_constellation_df
 
 
-def get_corpus(corpus_settings, data_path=DATA_PATH):
+def get_corpus(corpus_settings, data_dir=DATA_PATH):
 
     return Corpus(
         corpus_settings['corpus_name'],
-        registry_path=corpus_settings['registry_path'],
-        lib_path=corpus_settings.get('lib_path', None),
-        data_path=data_path
+        registry_dir=corpus_settings['registry_dir'],
+        lib_dir=corpus_settings.get('lib_dir', None),
+        data_dir=data_dir
     )
 
 
@@ -234,45 +234,48 @@ def test_create_constellation(germaparl, discoursemes):
     topic_discourseme = {
         'topic': topic_items
     }
-    discoursemes = discoursemes
 
     # filter
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 topic_discourseme,
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=corpus_name,
+        # discoursemes
+        topic_discourseme=topic_discourseme,
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assert len(const.df) == 10
 
     # highlight
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 topic_discourseme,
-                                 {},
-                                 discoursemes,
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=corpus_name,
+        # discoursemes
+        topic_discourseme=topic_discourseme,
+        filter_discoursemes={},
+        additional_discoursemes=discoursemes,
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assert len(const.df) == 2990
 
@@ -290,23 +293,24 @@ def test_create_textconstellation(germaparl, discoursemes):
     s_context = parameters['s_context']
     context = parameters['context']
 
-    # create constellation
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 {},
-                                 {},
-                                 discoursemes,
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=corpus_name,
+        # discoursemes
+        topic_discourseme={},
+        filter_discoursemes={},
+        additional_discoursemes=discoursemes,
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assert len(const.df) == 2198
 
@@ -327,22 +331,24 @@ def test_constellation_conc(germaparl, discoursemes):
     discoursemes = discoursemes
 
     # filter
-    const = create_constellation(germaparl['corpus_name'],
-                                 # discoursemes
-                                 topic_discourseme,
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 parameters['s_context'],
-                                 parameters['context'],
-                                 # query settings
-                                 parameters['p_query'],
-                                 parameters['s_query'],
-                                 parameters['flags_query'],
-                                 parameters['escape_query'],
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme=topic_discourseme,
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=parameters['s_context'],
+        context=parameters['context'],
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=parameters['p_query'],
+        s_query=parameters['s_query'],
+        flags=parameters['flags_query'],
+        escape=parameters['escape_query']
+    )
 
     lines = const.concordance(s_show=['text_id'])
 
@@ -365,22 +371,24 @@ def test_constellation_conc_htmlify_meta(germaparl, discoursemes):
     discoursemes = discoursemes
 
     # filter
-    const = create_constellation(germaparl['corpus_name'],
-                                 # discoursemes
-                                 topic_discourseme,
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 parameters['s_context'],
-                                 parameters['context'],
-                                 # query settings
-                                 parameters['p_query'],
-                                 parameters['s_query'],
-                                 parameters['flags_query'],
-                                 parameters['escape_query'],
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme=topic_discourseme,
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=parameters['s_context'],
+        context=parameters['context'],
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=parameters['p_query'],
+        s_query=parameters['s_query'],
+        flags=parameters['flags_query'],
+        escape=parameters['escape_query']
+    )
 
     lines = const.concordance(s_show=['text_id'], htmlify_meta=True)
 
@@ -408,14 +416,13 @@ def test_constellation_collocates(germaparl):
     additional_discoursemes = {}
     windows = [3, 5, 7]
     cqp_bin = 'cqp'
-    lib_path = None
+    lib_dir = None
     p_show = ['lemma']
     ams = None
     cut_off = 200
     min_freq = 2
     order = 'log_likelihood'
     escape = True
-    frequencies = True
     match_strategy = 'longest'
 
     # preprocess parameters
@@ -424,27 +431,31 @@ def test_constellation_collocates(germaparl):
 
     # create constellation
     const = create_constellation(
-        germaparl['corpus_name'],
-        {topic_name: topic_items},
-        {},
-        additional_discoursemes,
-        s_context,
-        context,
-        p_query,
-        s_query,
-        flags_query,
-        escape,
-        match_strategy,
-        lib_path,
-        cqp_bin,
-        germaparl['registry_path'],
-        DATA_PATH
-    )
-
-    collocates = const.collocates(windows=windows,
-                                  p_show=p_show, flags=flags_show,
-                                  ams=ams, frequencies=frequencies, min_freq=min_freq,
-                                  order=order, cut_off=cut_off)
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme={topic_name: topic_items},
+        filter_discoursemes={},
+        additional_discoursemes=additional_discoursemes,
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        match_strategy=match_strategy,
+        lib_dir=lib_dir,
+        cqp_bin=cqp_bin,
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags_query,
+        escape=escape
+    )
+
+    collocates = const.collocates(windows=windows, p_show=p_show,
+                                  flags=flags_show, ams=ams,
+                                  min_freq=min_freq, order=order,
+                                  cut_off=cut_off)
 
     assert len(collocates) == 3
 
@@ -515,6 +526,7 @@ def test_constellation_coll(germaparl, discoursemes):
 ##########################
 # TEXTUAL CONSTELLATIONS #
 ##########################
+@pytest.mark.now
 def test_textual_constellation(germaparl, discoursemes):
 
     corpus = get_corpus(germaparl)
@@ -539,6 +551,8 @@ def test_textual_constellation(germaparl, discoursemes):
     assert len(const.df) == 624
     assert 'topic' in const.df.columns
 
+    print(const.df)
+
 
 def test_textual_constellation_add(germaparl, discoursemes):
 
@@ -585,8 +599,6 @@ def test_textual_constellation_add(germaparl, discoursemes):
 
 def test_textual_constellation_association(germaparl, discoursemes):
 
-    corpus_name = germaparl['corpus_name']
-
     # parameters
     parameters = discoursemes.pop('parameters')
     flags = parameters['flags_query']
@@ -596,22 +608,24 @@ def test_textual_constellation_association(germaparl, discoursemes):
     s_context = parameters['s_context']
     context = parameters['context']
 
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 {},
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme={},
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assoc = const.associations()
     assert len(assoc) == 6
@@ -620,8 +634,6 @@ def test_textual_constellation_association(germaparl, discoursemes):
 
 def test_textual_constellation_association_approximate(germaparl, discoursemes):
 
-    corpus_name = germaparl['corpus_name']
-
     # parameters
     parameters = discoursemes.pop('parameters')
     flags = parameters['flags_query']
@@ -631,23 +643,25 @@ def test_textual_constellation_association_approximate(germaparl, discoursemes):
     s_context = parameters['s_context']
     context = parameters['context']
 
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 {},
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH,
-                                 approximate=True)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme={},
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=s_context,
+        context=context,
+        approximate=True,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assoc = const.associations()
     assert len(assoc) == 6
@@ -656,8 +670,6 @@ def test_textual_constellation_association_approximate(germaparl, discoursemes):
 
 def test_textual_constellation_association_empty(germaparl, discoursemes):
 
-    corpus_name = germaparl['corpus_name']
-
     # parameters
     parameters = discoursemes.pop('parameters')
     flags = parameters['flags_query']
@@ -670,23 +682,25 @@ def test_textual_constellation_association_empty(germaparl, discoursemes):
     discoursemes2['fail'] = ["fail"]
     discoursemes2['fail2'] = ["fail2"]
 
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 {},
-                                 discoursemes2,
-                                 {},
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH,
-                                 approximate=True)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme={},
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=s_context,
+        context=context,
+        approximate=True,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assoc = const.associations()
     assert len(assoc) == 6
@@ -695,8 +709,6 @@ def test_textual_constellation_association_empty(germaparl, discoursemes):
 
 def test_textual_constellation_concordance(germaparl, discoursemes):
 
-    corpus_name = germaparl['corpus_name']
-
     # parameters
     parameters = discoursemes.pop('parameters')
     flags = parameters['flags_query']
@@ -707,22 +719,24 @@ def test_textual_constellation_concordance(germaparl, discoursemes):
     context = parameters['context']
 
     # create constellation
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 {},
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme={},
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     # retrieve lines
     lines = const.concordance(cut_off=None)
@@ -732,8 +746,6 @@ def test_textual_constellation_concordance(germaparl, discoursemes):
 
 def test_textual_constellation_breakdown(germaparl, discoursemes):
 
-    corpus_name = germaparl['corpus_name']
-
     # parameters
     parameters = discoursemes.pop('parameters')
     flags = parameters['flags_query']
@@ -744,21 +756,23 @@ def test_textual_constellation_breakdown(germaparl, discoursemes):
     context = parameters['context']
 
     # create constellation
-    const = create_constellation(corpus_name,
-                                 # discoursemes
-                                 {},
-                                 discoursemes,
-                                 {},
-                                 # context settings
-                                 s_context,
-                                 context,
-                                 # query settings
-                                 p_query,
-                                 s_query,
-                                 flags,
-                                 escape,
-                                 # CWB setttings
-                                 registry_path=germaparl['registry_path'],
-                                 data_path=DATA_PATH)
+    const = create_constellation(
+        corpus_name=germaparl['corpus_name'],
+        # discoursemes
+        topic_discourseme={},
+        filter_discoursemes=discoursemes,
+        additional_discoursemes={},
+        # context settings
+        s_context=s_context,
+        context=context,
+        # CWB setttings
+        registry_dir=germaparl['registry_dir'],
+        data_dir=DATA_PATH,
+        # query settings
+        p_query=p_query,
+        s_query=s_query,
+        flags=flags,
+        escape=escape
+    )
 
     assert len(const.breakdown()) == 5