From 0c28d16067d5cb076ebe71155ffdb408c45de6df Mon Sep 17 00:00:00 2001 From: David WF Date: Mon, 21 Aug 2023 16:21:19 -0700 Subject: [PATCH 1/7] Try adding spellcheck (expecting a failure) --- .github/workflows/spellcheck.yml | 14 ++ docs/.gitignore | 3 +- docs/.pyspelling.yml | 29 +++ docs/.wordlist.txt | 182 ++++++++++++++++++ docs/README.md | 28 +-- docs/examples/datasets/datasets.py | 2 +- docs/pages/api-reference/aggregations.md | 4 +- docs/pages/api-reference/client.md | 12 +- docs/pages/api-reference/expectations.md | 12 +- docs/pages/api-reference/operators.md | 4 +- docs/pages/api-reference/sources.md | 4 +- docs/pages/architecture/cost-optimizations.md | 12 +- docs/pages/architecture/deployment-model.md | 4 +- docs/pages/architecture/overview.md | 34 ++-- docs/pages/architecture/privacy-security.md | 10 +- .../architecture/read-write-separation.md | 10 +- docs/pages/concepts/featureset.md | 44 ++--- docs/pages/concepts/pipeline.md | 58 +++--- docs/pages/concepts/source.md | 84 ++++---- docs/pages/data-quality/lineage-validation.md | 18 +- docs/pages/data-quality/metaflags.md | 42 ++-- docs/pages/data-quality/strong-typing.md | 10 +- docs/pages/development/monitoring-alerting.md | 20 +- docs/pages/getting-started/quickstart.md | 40 ++-- docs/pages/guides/lambda-architecture.md | 4 +- docs/pages/guides/request-based-features.md | 10 +- docs/pages/misc/troubleshooting-guide.md | 10 +- 27 files changed, 465 insertions(+), 239 deletions(-) create mode 100644 .github/workflows/spellcheck.yml create mode 100644 docs/.pyspelling.yml create mode 100644 docs/.wordlist.txt diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml new file mode 100644 index 000000000..d4dd922aa --- /dev/null +++ b/.github/workflows/spellcheck.yml @@ -0,0 +1,14 @@ +name: Spellcheck + +on: [pull_request, push] + +jobs: + checks: + runs-on: ubuntu-latest + name: Spellcheck + steps: + - name: Check out source repository + uses: actions/checkout@v3 + - uses: rojopolis/spellcheck-github-actions@v0 + working-directory: docs/ + name: Spellcheck diff --git a/docs/.gitignore b/docs/.gitignore index e9c49fbc2..b80f993e9 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,4 +3,5 @@ examples/**.json .vscode venv/ **/__pycache__/* -.idea/ \ No newline at end of file +.idea/ +wordlist.dic diff --git a/docs/.pyspelling.yml b/docs/.pyspelling.yml new file mode 100644 index 000000000..5bffc1505 --- /dev/null +++ b/docs/.pyspelling.yml @@ -0,0 +1,29 @@ +matrix: + - name: Python source + sources: + - examples/**/*.py + dictionary: + wordlists: + - .wordlist.txt + output: wordlist.dic + encoding: utf-8 + pipeline: + - pyspelling.filters.python: + - name: markdown + sources: + - '**/*.md' + dictionary: + wordlists: + - .wordlist.txt + output: wordlist.dic + pipeline: + - pyspelling.filters.markdown: + - pyspelling.filters.html: + comments: false + attributes: + - title + - alt + ignores: + - code + - pre + - pyspelling.filters.url: diff --git a/docs/.wordlist.txt b/docs/.wordlist.txt new file mode 100644 index 000000000..f31498154 --- /dev/null +++ b/docs/.wordlist.txt @@ -0,0 +1,182 @@ +APIs +AST +AdministratorAccess +Avro +CIDR +DDL +DSL +DSLs +DataFrame +DataFrames +Datadog +Dockerfile +Flink +GCP +GCP's +GRPC +Github +Grafana +Graviton +Groupby +IOPS +InfoSec +Instacart +JSON +JSX +JVM +Kaggle +Kubernetes +LHS +LastK +MockClient +Nones +OAuth +OOM +PII +PagerDuty +PoolableConnectionFactory +PrivateLink +Pulumi +PyO +Pydantic +RHS +ROI +RPCs +Realtimeliness +RocksDB +SDK +SLA +SearchRequest +Signifier +Stddev +TLS +TLSv +TestCase +TestDataset +Tokio +Tokio's +UI +UserCreator +UserCreditScore +UserFeature +UserFeatures +UserInfo +UserInfoDataset +UserLocation +UserPost +UserTransactionsAbroad +VPC +WIP +WIP +YAML +ai +api +architected +assertEqual +async +autoscaling +backend +backfill +backfilled +bmi +bmr +bool +boolean +booleans +classmethod +classmethods +codebase +codepaths +compilable +config +configs +csv +dataclass +dataflow +dataframe +dataset +dataset's +datasets +datastore +datastores +datetime +dateutil +declaratively +dedup +denormalize +dev +df +dfe +docsnip +ds +durations +embeddings +enabledTLSProtocols +featureset +featuresets +fintech +frontend +geocoding +geoid +groupby +gserviceaccount +hackathon +hardcoded +html +hudi +iam +ip +ish +ith +jdbc +json +kafka +kwarg +kwargs +latencies +lifecycle +lookup +lookups +metaflags +multicolumn +mysql +nan +natively +noqa +np +nullable +params +parseable +pid +postgres +pre +precompute +precomputed +protobuf +protobufs +quickstart +realtime +regex +regexes +repo +runtime +scalability +scalable +schemas +signup +snowflakecomputing +stateful +str +strftime +struct +tiering +uid +uint +uints +uncomment +unittest +uptime +uptimes +userid +webhook +webhooks diff --git a/docs/README.md b/docs/README.md index 0a7b3f64f..495e3061e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,32 +7,32 @@ To get up and running contributing to the documentation, take the following step 2. Rename `.env.example` to `.env` and fill out the values. - `GITHUB_TOKEN` should be a valid Github PAT with access to read the `fennel-ai/turbo` repo - `GITHUB_REPO` is the location of the Dockerfile that builds the frontend, and should be set to `fennel-ai/turbo` -3. Run in your terminal `make up` from the root - - This will pull in the Docs UI repo from Github, and run it on `localhost:3001/docs` +3. Run in your terminal `make up` from the root + - This will pull in the Docs UI repo from Github, and run it on `localhost:3001/docs` 4. Edit the markdown and python files in this repo, and get hot-reloading showing the latest changes on `localhost` 5. Commit changes once you're ready. - - Upon commit, the python files will run through the test suite and block any broken examples from going live in the documentation. + - Upon commit, the python files will run through the test suite and block any broken examples from going live in the documentation. -> When new updates are made to the UI, you may need to run `make build` before `make up` in order to force Docker to refetch the latest changes and rebuild the image. +> When new updates are made to the UI, you may need to run `make build` before `make up` in order to force Docker to fetch the latest changes and rebuild the image. ## `./examples` -The example directory holds Python test files. Anywhere in these files you can wrap any number of lines between `# docsnip` comments +The example directory holds Python test files. Anywhere in these files you can wrap any number of lines between `# docsnip` comments **e.g.** `example.py`: ```python -from fennel import * +from fennel import * # docsnip my_snippet @dataset class UserInfoDataset: - name: str - email: str - id: str - age: int + name: str + email: str + id: str + age: int # /docsnip - def my_pipeline(): - # todo - return False + def my_pipeline(): + # todo + return False ``` Now, in any of our markdown files you can write: @@ -55,4 +55,4 @@ The `index.yml` file is used to set global configuration options for the docs. C Any pages that are _not_ in the file are still generated in dev and production (if they are not a `draft`) and can be navigated/linked to, but won't appear in the sidebar. -The `version` field gives us a way to easily pick out the version tag for this branch of the documentation from the UI side. \ No newline at end of file +The `version` field gives us a way to easily pick out the version tag for this branch of the documentation from the UI side. diff --git a/docs/examples/datasets/datasets.py b/docs/examples/datasets/datasets.py index 83c3e30ed..276340586 100644 --- a/docs/examples/datasets/datasets.py +++ b/docs/examples/datasets/datasets.py @@ -90,7 +90,7 @@ class User: # invalid - no explicitly marked `timestamp` field # and multiple fields of type `datetime` so timestamp -# field is amgiguous +# field is ambiguous def test_ambiguous_timestamp_field(): with pytest.raises(Exception) as e: # docsnip invalid_user_dataset_ambiguous_timestamp_field diff --git a/docs/pages/api-reference/aggregations.md b/docs/pages/api-reference/aggregations.md index a2966b5ea..882b54057 100644 --- a/docs/pages/api-reference/aggregations.md +++ b/docs/pages/api-reference/aggregations.md @@ -6,7 +6,7 @@ status: 'published' # Aggregations -Aggregations are provided to the \`aggregate\` operator and specify how the agggregation should happen. All aggregations take two common arguments: +Aggregations are provided to the \`aggregate\` operator and specify how the aggregation should happen. All aggregations take two common arguments: 1. `window`: Window - argument that specifies the length of the duration across which Fennel needs to perform the aggregation. See how [duration](/api-reference/duration) is specified in Fennel. 2. `into_field`: str - the name of the field in the output dataset that corresponds to this aggregation. This @@ -18,7 +18,7 @@ Besides these common arguments, here is the rest of the API reference for all th Count computes a rolling count for each group key across a window. It returns 0 by default. Its output type is always `int`. The count aggregate also takes an optional argument `unique` which is a boolean. If set to true, counts the number of unique values in the given window. The field over which the count is computed is specified by the `of` parameter of type `str`. -Count also takes `approx` as an argument that when set to true, makes the count an approximate, but allows Fennel to be more efficient with state storage. +Count also takes `approx` as an argument that when set to true, makes the count an approximate, but allows Fennel to be more efficient with state storage. Currently, Fennel only supports approximate unique counts, hence if `unique` is set to true, `approx` must also be set to true. ### 2. Sum diff --git a/docs/pages/api-reference/client.md b/docs/pages/api-reference/client.md index cd99046b4..fb8383e3b 100644 --- a/docs/pages/api-reference/client.md +++ b/docs/pages/api-reference/client.md @@ -1,7 +1,7 @@ --- title: Client order: 0 -status: wip +status: WIP --- # Client @@ -14,7 +14,7 @@ Given some input and output features, extracts the current value of all the outp **Arguments:** -* `output_feature_list: List[Union[Feature, Featureset]]`: list of features (written as fully qualified name of a feature along with the featureset) that should be extracted. Can also take featurset objects as input, in which case all features in the featureset are extracted. +* `output_feature_list: List[Union[Feature, Featureset]]`: list of features (written as fully qualified name of a feature along with the featureset) that should be extracted. Can also take featureset objects as input, in which case all features in the featureset are extracted. * `input_feature_list: List[Union[Feature, Featureset]]` : list of features/featuresets for which values are known * `input_df: Dataframe`: a pandas dataframe object that contains the values of all features in the input feature list. Each row of the dataframe can be thought of as one entity for which features are desired. * `log: bool` - boolean which indicates if the extracted features should also be logged (for log-and-wait approach to training data generation). Default is False @@ -73,11 +73,11 @@ This method throws an error if the schema of the dataframe (i.e. column names an ### **extract_historical_features** -For offline training of models, users often need to extract features for a large number of entities. +For offline training of models, users often need to extract features for a large number of entities. This method allows users to extract features for a large number of entities in a single call while ensuring point-in-time correctness of the extracted features. -This api is an asynchronous api that returns a request id and the path to the output folder in S3 containing the extracted features. +This api is an asynchronous api that returns a request id and the path to the output folder in S3 containing the extracted features. **Arguments:** @@ -112,7 +112,7 @@ A completion rate of 1.0 and a failure rate of 0.0 indicates that all processing ### **extract_historical_features_progress** -This method allows users to monitor the progress of the extract_historical_features asynchronous operation. +This method allows users to monitor the progress of the extract_historical_features asynchronous operation. It accepts the request ID that was returned by the `extract_historical_features` method and returns the current status of that operation. The response format of this function and the `extract_historical_features` function are identical. @@ -129,7 +129,7 @@ The response format of this function and the `extract_historical_features` funct * request_id * output s3 bucket * output s3 path prefix - * completion rate. + * completion rate. * failure rate. A completion rate of 1.0 indicates that all processing has been completed. diff --git a/docs/pages/api-reference/expectations.md b/docs/pages/api-reference/expectations.md index c263d7a29..112d094c2 100644 --- a/docs/pages/api-reference/expectations.md +++ b/docs/pages/api-reference/expectations.md @@ -13,7 +13,7 @@ provides the ability to specify expectations on the data. Fennel internally relies on [Great Expectations](https://greatexpectations.io/) to help users easily specify data expectations. Fennel's expectations are a subset of `Great Expectations` -expectations and are documented below, but the api to specify expectations is the same. +expectations and are documented below, but the api to specify expectations is the same. ## Expectation Types @@ -154,9 +154,9 @@ The following expectations operate on a single column at a time.
  • [expect_column_values_to_not_match_regex_list](https://greatexpectations.io/expectations/expect_column_values_to_not_match_regex_list) - + Expect column entries to be strings that do not match any of a list of regular expressions.

    - + *Parameters*: - `column (str)` – The column name. - `regex_list (list)` – The list of regular expressions that each column entry should not match any of. @@ -189,7 +189,7 @@ The following expectations operate on a single column at a time. [expect_column_values_to_be_json_parseable](https://greatexpectations.io/expectations/expect_column_values_to_be_json_parseable) Expect column entries to be parseable as JSON.

    - + *Parameters*: - `column (str)` – The column name.
    @@ -199,7 +199,7 @@ The following expectations operate on a single column at a time. [expect_column_values_to_match_json_schema](https://greatexpectations.io/expectations/expect_column_values_to_match_json_schema) Expect column entries to match a given JSON schema.

    - + *Parameters*: - `column (str)` – The column name. - `json_schema (dict)` – The JSON schema that each column entry should match. @@ -209,7 +209,7 @@ The following expectations operate on a single column at a time. ### Multi Column Expectations -The following expectations require two or more columns. +The following expectations require two or more columns.
    1. diff --git a/docs/pages/api-reference/operators.md b/docs/pages/api-reference/operators.md index 3253681c7..a9b1fde6a 100644 --- a/docs/pages/api-reference/operators.md +++ b/docs/pages/api-reference/operators.md @@ -1,7 +1,7 @@ --- title: Operators order: 4 -status: wip +status: WIP --- # Operators @@ -104,7 +104,7 @@ The `aggregate` operator has the following parameters: ### Groupby/ First Fennel allows you to groupby and get the first entry (by timestamp) in each group using the `first` operator. -The `first` operator must be preceded by `groupby` to idenitfy the grouping fields. +The `first` operator must be preceded by `groupby` to identify the grouping fields. `groupby` takes the following parameters: diff --git a/docs/pages/api-reference/sources.md b/docs/pages/api-reference/sources.md index 39e3da1e9..545908f01 100644 --- a/docs/pages/api-reference/sources.md +++ b/docs/pages/api-reference/sources.md @@ -31,7 +31,7 @@ The following fields need to be specified: 1. **`name`** - A name to identify the source. The name should be unique across all sources. 2. **`host`** - The host name of the database. -3. **`port`** - The port to connect to. By default it is 3303 for MySQL and 5432 for Posgres. +3. **`port`** - The port to connect to. By default it is 3303 for MySQL and 5432 for Postgres. 4. **`db_name`** - The database name. 5. **`username`** - The username which is used to access the database. 6. **`password`** - The password associated with the username. @@ -103,7 +103,7 @@ Interfacing with BigQuery requires credentials for a [Service Account](https://cloud.google.com/iam/docs/service-accounts) with the "BigQuery User" and "BigQuery Data Editor" roles, which grants permissions to run BigQuery jobs, write to BigQuery Datasets, and read table metadata. It is highly recommended that this Service Account is exclusive to Fennel for ease of permissions and auditing. However, you -can also use a pre-existing Service Account if you already have one with the correct permissions. +can also use a preexisting Service Account if you already have one with the correct permissions. The easiest way to create a Service Account is to follow GCP's guide for [Creating a Service Account](https://cloud.google.com/iam/docs/creating-managing-service-accounts). Once you've diff --git a/docs/pages/architecture/cost-optimizations.md b/docs/pages/architecture/cost-optimizations.md index ec7128165..e2e1aa41e 100644 --- a/docs/pages/architecture/cost-optimizations.md +++ b/docs/pages/architecture/cost-optimizations.md @@ -7,10 +7,10 @@ status: 'published' # Cost Optimizations Production feature engineering systems deal with lots of data at low latencies -and hence can be very costly in terms of cloud infrastructure costs. Fennel -goes an extra mile in trying to keep cloud costs low by squeezing as much out -of cloud hardware as possible. This is accomplished by a long tail of cost -optimizations -- many of which are too low ROI for each team to invest in +and hence can be very costly in terms of cloud infrastructure costs. Fennel +goes an extra mile in trying to keep cloud costs low by squeezing as much out +of cloud hardware as possible. This is accomplished by a long tail of cost +optimizations -- many of which are too low ROI for each team to invest in individually but become feasible due to economies of scale enjoyed by Fennel. Here is a non-exhaustive list of such optimizations: @@ -20,10 +20,10 @@ Here is a non-exhaustive list of such optimizations: * Using minimal managed services provided by cloud vendors and instead running the open source versions on our own on top of just the EC2 (this avoids 2-3x markup charged by cloud vendors) * Using efficient Rust to power all services to reduce CPU demands * In particular, not relying on more general purpose streaming systems like spark or Flink but using an in-house Rust based system purpose built for feature engineering workloads with much lower overhead -* Using AWS graviton processor based instances which offer better price/performance ratio +* Using AWS Graviton processor based instances which offer better price/performance ratio * Choosing the right instance family and size for each sub-system * Auto scaling up/down various clusters depending on the workload (e.g. reduce costs at night) -* Tightly encoding data in binary formats (e.g. using variable length ints etc.) in all storage engines to reduce storage and network bandwidth costs +* Tightly encoding data in binary formats (e.g. using variable length integers, etc.) in all storage engines to reduce storage and network bandwidth costs * Adding compression (say at disk block level) in data storage systems * Data tiering - preferring to keeping data in S3 vs instance store vs RAM whenever possible * Avoiding network costs by preferably talking to data sources in the same AZ diff --git a/docs/pages/architecture/deployment-model.md b/docs/pages/architecture/deployment-model.md index b565101d6..131828700 100644 --- a/docs/pages/architecture/deployment-model.md +++ b/docs/pages/architecture/deployment-model.md @@ -28,5 +28,5 @@ cloud. 6. Sit back as Fennel deploys its Feature Engineering Platform in this account and manages it from there. To deploy and perform ongoing maintenance and upgrades, Fennel peers the deployed VPC with a VPC in Fennel's account (a.k.a. the control plane). -7. Access the Fennnel service from your production/dev account via AWS PrivateLink - or VPC peering if a peering connection was established in step 5. \ No newline at end of file +7. Access the Fennel service from your production/dev account via AWS PrivateLink + or VPC peering if a peering connection was established in step 5. diff --git a/docs/pages/architecture/overview.md b/docs/pages/architecture/overview.md index 22a22224e..674361f22 100644 --- a/docs/pages/architecture/overview.md +++ b/docs/pages/architecture/overview.md @@ -6,21 +6,21 @@ status: 'published' # Overview -Here are some of the key ideas & principles behind Fennel's architecture that +Here are some of the key ideas & principles behind Fennel's architecture that allow it to meet its [design goals](/): ## Read Write Separation -This is arguably the most critical architecture decision that differentiates -Fennel from many other similar systems. You can read about this in more detail +This is arguably the most critical architecture decision that differentiates +Fennel from many other similar systems. You can read about this in more detail [here](/architecture/read-write-separation). ## Kappa Architecture Fennel uses a Kappa like architecture to operate on streaming and offline data. This enables Fennel to maintain a single code path to power the data operations -and have the same pipeline declarations work seamlessly in both realtime and -batch sources. This side-steps a lot of issues intrinsic to the Lambda +and have the same pipeline declarations work seamlessly in both realtime and +batch sources. This side-steps a lot of issues intrinsic to the Lambda architecture, which is the most mainstream alternative to Kappa. ## Hybrid Materialized Views @@ -28,38 +28,38 @@ architecture, which is the most mainstream alternative to Kappa. To reduce read latencies, data on the write path is pre-materialized and stored in datasets. The main downside of materializing views is that it may lead to wasted computation and storage for data that is never read. Fennel's read write -separation minimizes this downside by giving control to the end user for what +separation minimizes this downside by giving control to the end user for what computation to pre-materialize and what computation to on the read path. ## Minimal Sync Communication for Horizontal Scaling Every single subsystem within Fennel is designed with horizontal scalability in mind. While ability to scale out is usually desirable, if not done well, lots of independent -nodes can lead to inter-node overheads leading to capacity of the system not growing +nodes can lead to inter-node overheads leading to capacity of the system not growing linearly with hardware capacity. It also creates failure modes like cascades of failures. Fennel minimizes these by reducing cross-node sync communication - it does so by keeping -some local state with each node (which needs no communication), keeping global metadata -in centrally accessible Postgres, and making all communication async - within node +some local state with each node (which needs no communication), keeping global metadata +in centrally accessible Postgres, and making all communication async - within node communication via async Rust channels and cross-node communication via Kafka (vs sync RPCs) ## Async Rust (using Tokio) All Fennel services are written in Rust with tight control over CPU and memory -footprints. Further, since feature engineering is a very IO heavy workload on +footprints. Further, since feature engineering is a very IO heavy workload on both read and write sides, Fennel heavily depends on async Rust to release CPU for other tasks while one task is waiting on some resource. For instance, each `job` in Fennel's streaming system gets managed as an async task enabling many -jobs to share the same CPU core with minimal context switch overhead. This +jobs to share the same CPU core with minimal context switch overhead. This enables Fennel to efficiently utilize all the CPU available to it. ## Embedded Python -Fennel's philosophy is to let users write in real Python with their familiar +Fennel's philosophy is to let users write in real Python with their familiar libraries vs having to learn new DSLs. But this requires lot of back and forth -between Python land (which is bound by GIL) and the rest of the Rust system. -Fennel handles this by embedding a Python interpreter inside Rust binaries -(via PyO3). This enables Fennel to cross the Python/Rust boundary very -cheaply, while still being async. [PEP 684](https://discuss.python.org/t/pep-684-a-per-interpreter-gil/19583) -further allows Fennel to eliminate GIL bottleneck by embedding multiple +between Python land (which is bound by GIL) and the rest of the Rust system. +Fennel handles this by embedding a Python interpreter inside Rust binaries +(via PyO3). This enables Fennel to cross the Python/Rust boundary very +cheaply, while still being async. [PEP 684](https://discuss.python.org/t/pep-684-a-per-interpreter-gil/19583) +further allows Fennel to eliminate GIL bottleneck by embedding multiple Python sub-interpreters in Rust threads. diff --git a/docs/pages/architecture/privacy-security.md b/docs/pages/architecture/privacy-security.md index 52836833f..c239571ec 100644 --- a/docs/pages/architecture/privacy-security.md +++ b/docs/pages/architecture/privacy-security.md @@ -8,11 +8,11 @@ status: 'published' ### General InfoSec -Fennel runs inside your VPC and thus is subject to your usual infosec policies. +Fennel runs inside your VPC and thus is subject to your usual InfoSec policies. The code or data never leave your cloud which eliminates many privacy/compliance -vulnurabilities. +vulnerabilities. -Fennel uses PrivateLink for creating endpoints - as a result, the endpoints aren't +Fennel uses PrivateLink for creating endpoints - as a result, the endpoints aren't visible to the public internet outside of your VPC. While Fennel's control plane runs outside of your VPC, it only has access to logs @@ -24,6 +24,6 @@ and telemetry information. Fennel uses industry best-practices for data security: * All data is encrypted in transit and at rest -* Secure strorage of user-provided secrets in secrete stores or encrypted disks +* Secure storage of user-provided secrets in secrete stores or encrypted disks * All inter-server communication happens over TLS -* Authentication and TLS for client-server requests \ No newline at end of file +* Authentication and TLS for client-server requests diff --git a/docs/pages/architecture/read-write-separation.md b/docs/pages/architecture/read-write-separation.md index 1ced02999..9b68852a1 100644 --- a/docs/pages/architecture/read-write-separation.md +++ b/docs/pages/architecture/read-write-separation.md @@ -6,16 +6,16 @@ status: 'published' # Read/Write Separation -Unlike most (if not all?) other feature platforms out there, Fennel distinguishes between read and write path computation. This is so because these are very different from perf characteristics - +Unlike most (if not all?) other feature platforms out there, Fennel distinguishes between read and write path computation. This is so because these are very different from a performance perspective - * Write path is throughput bound but does not have latency pressure whereas Read path is very sensitive to latency but often operates on tiny batches. * Write path creates data and so can end up taking lots of storage space, some of which may be wasted if the data isn't actually read in a request. That storage space may be traded off with CPU by moving that computation to the read path which may repeat the same computation for each request. -What computation to do on write path vs read path is a very application specific decision and so Fennel gives you control on that decision - pre-compute some quantity on the write path by keeping it in a pipeline. Or compute it on the fly by writing it as an extractor. +What computation to do on write path vs read path is a very application specific decision and so Fennel gives you control on that decision - precompute some quantity on the write path by keeping it in a pipeline. Or compute it on the fly by writing it as an extractor. Note that in a majority of cases, you'd just want to move the computation to write side as a pipeline. But there are several reasons/cases where keeping the computation on read path may make sense. Here are some examples: * **Sparsity** -- say you have a feature that does dot product between user and content embeddings. There are 1M users and 1M content. It's very wasteful to compute dot product between every pair of user/content because it will take a lot of storage and most of it will not be read ever. So it's better to lookup embeddings from datasets and do their dot product in the extractor. -* **CPU cost** -- say you have a model based feature and further it's a heavy neural network model. You could put it on the write path to save latency. But that also means that you're running this neural network on every row of dataset, most of which may never be seen in prod. So depending on the CPU vs latency tradeoff, it may make sense to keep it on the read path. -* **Request properties** -- for features that depend on request specific properties e.g. "is the transaction amount larger than user's average over the last 1 day", you may have no choice but to look up some partially pre-computed info and do some read side computation it. -* **Temporal features** - say you have a feature that represents user's age. The values of this feature update automatically with passing of time and so it's physically impossible to pre-compute this on the write path. A more natural way is to look up user's date of birth from a dataset and subtract that from the current time. +* **CPU cost** -- say you have a model based feature and further it's a heavy neural network model. You could put it on the write path to save latency. But that also means that you're running this neural network on every row of dataset, most of which may never be seen in prod. So depending on the CPU vs latency trade-off, it may make sense to keep it on the read path. +* **Request properties** -- for features that depend on request specific properties e.g. "is the transaction amount larger than user's average over the last 1 day", you may have no choice but to look up some partially precomputed info and do some read side computation it. +* **Temporal features** - say you have a feature that represents user's age. The values of this feature update automatically with passing of time and so it's physically impossible to precompute this on the write path. A more natural way is to look up user's date of birth from a dataset and subtract that from the current time. diff --git a/docs/pages/concepts/featureset.md b/docs/pages/concepts/featureset.md index 2c2a23a79..11dba5241 100644 --- a/docs/pages/concepts/featureset.md +++ b/docs/pages/concepts/featureset.md @@ -8,7 +8,7 @@ status: 'published' Featuresets refer to a group of logically related features where each feature is backed by a Python function that knows how to extract it. A featureset is written -as a Python class annotated with `@featureset` decorator. A single application +as a Python class annotated with `@featureset` decorator. A single application will typically have many featuresets. Let's see an example: ### Example @@ -16,20 +16,20 @@ will typically have many featuresets. Let's see an example:
      
       
       
      -Above example defines a featureset called `Movie` with two features - `duration`, 
      +Above example defines a featureset called `Movie` with two features - `duration`,
       `over_2hrs`. Each feature has a [type](/api-reference/data-types) and is given
      -a monotonically increasing `id` that is unique within the featureset. This 
      -featureset has one extractor - `my_extractor` that when given the `duration` 
      -feature, knows how to extract the `over_2hrs` feature. Let's look at extractors in 
      +a monotonically increasing `id` that is unique within the featureset. This
      +featureset has one extractor - `my_extractor` that when given the `duration`
      +feature, knows how to extract the `over_2hrs` feature. Let's look at extractors in
       a bit more detail.
       
       
       ### Extractors
       
      -Extractors are stateless Python functions in a featureset that are annotated 
      -by `@extractor` decorator. Each extractor accepts zero or more inputs 
      -(marked in `inputs` decorator) and produces one or more features (marked in 
      -`outputs` decorator). 
      +Extractors are stateless Python functions in a featureset that are annotated
      +by `@extractor` decorator. Each extractor accepts zero or more inputs
      +(marked in `inputs` decorator) and produces one or more features (marked in
      +`outputs` decorator).
       
       In the above example, if the value of feature `durations` is known, `my_extractor`
       can extract the value of `over_2hrs`. When you are interested in getting values of
      @@ -48,11 +48,11 @@ this (assuming 3 input features and 2 output features):
       | Jan 12, 2022, 8:30am  | 456     | 'world'  | True    | ?        | ?        |
       | Jan 13, 2022, 10:15am | 789     | 'fennel' | False   | ?        | ?        |
       
      -The output is supposed to be the value of that feature asof the given timestamp
      -assuming other input feature values to be as given. 
      +The output is supposed to be the value of that feature as of the given timestamp
      +assuming other input feature values to be as given.
       
      -Extractor is a classmethod so its first argument is always `cls`. After that, the 
      -second argument is always a series of timestamps (as shown above) and after that, 
      +Extractor is a classmethod so its first argument is always `cls`. After that, the
      +second argument is always a series of timestamps (as shown above) and after that,
       it gets one series for each input feature. The output is a named series or dataframe,
       depending on the number of output features, of the same length as the input features.
       
      @@ -62,9 +62,9 @@ depending on the number of output features, of the same length as the input feat
       
       1. A featureset can have zero or more extractors.
       2. An extractor can have zero or more inputs but must have at least one output.
      -3. Input features of an extractor can belong to any number of featuresets but all 
      +3. Input features of an extractor can belong to any number of featuresets but all
          output features must be from the same featureset as the extractor.
      -4. For any feature, there can be at most one extractor where the feature 
      +4. For any feature, there can be at most one extractor where the feature
          appears in the output list.
       
       With this, let's look at a few valid and invalid examples:
      @@ -76,7 +76,7 @@ Valid - featureset can have multiple extractors provided they are all valid
       
       
      
       
      -Invalid - multiple extractors extracting the same feature. `over_3hrs` is 
      +Invalid - multiple extractors extracting the same feature. `over_3hrs` is
       extracted both by e1 and e2:
       
      
       
      @@ -89,8 +89,8 @@ Invalid - output feature of extractor from another featureset
       
       ### Dataset Lookups
       
      -A large fraction of real world ML features are built on top of stored data. 
      -However, featuresets don't have any storage of their own and are completely 
      +A large fraction of real world ML features are built on top of stored data.
      +However, featuresets don't have any storage of their own and are completely
       stateless. Instead, they are able to do random lookups on datasets and use
       that for the feature computation. Let's see an example:
       
      
      @@ -101,10 +101,10 @@ of things:
       * In line 15, extractor has to explicitly declare that it depends on `User` dataset.
         This helps Fennel build an explicit lineage between features and the datasets they
         depend on.
      -* In line 19, extractor is able to call a `lookup` function on the dataset. This 
      +* In line 19, extractor is able to call a `lookup` function on the dataset. This
        function also takes series of timestamps as the first argument - you'd almost always
      - pass the extractor's timestamp list to this function as it is. In addition, all the 
      - key fields in the dataset become kwarg to the lookup function. 
      + pass the extractor's timestamp list to this function as it is. In addition, all the
      + key fields in the dataset become kwarg to the lookup function.
       * It's not possible to do lookups on dataset without keys.
       
      -  
      \ No newline at end of file
      +
      diff --git a/docs/pages/concepts/pipeline.md b/docs/pages/concepts/pipeline.md
      index 9d64a5cd8..d48330e43 100644
      --- a/docs/pages/concepts/pipeline.md
      +++ b/docs/pages/concepts/pipeline.md
      @@ -16,8 +16,8 @@ Imagine we have the following datasets defined in the system:
       
      
       
       
      -And we want to create a dataset which represents some stats about the 
      -transactions made by a user in a country different from their home country. 
      +And we want to create a dataset which represents some stats about the
      +transactions made by a user in a country different from their home country.
       We'd write that dataset as follows:
       
       
      
      @@ -26,22 +26,22 @@ There is a lot happening here so let's break it down line by line:
       
       * Lines 1-9 are defining a regular dataset. Just note that this dataset has the
        schema that we desire to create via the pipeline.
      -* Lines 14-32 describe the actual pipeline code - we'd come to that in a second. 
      -* Line 11 declares that this is a classmethod - all pipelines are classmethods. 
      -`pipeline` decorator itself wraps `classmethod` decorator so you can omit 
      -`classmethod` in practice - here it is shown for just describing the concept. 
      +* Lines 14-32 describe the actual pipeline code - we'd come to that in a second.
      +* Line 11 declares that this is a classmethod - all pipelines are classmethods.
      +`pipeline` decorator itself wraps `classmethod` decorator so you can omit
      +`classmethod` in practice - here it is shown for just describing the concept.
       * Line 12 declares that the decorated function represents a pipeline.
      -* Line 13 declares all the input datasets that are needed for this pipeline to 
      - derive the output dataset. In this case, this pipeline is declaring that it 
      +* Line 13 declares all the input datasets that are needed for this pipeline to
      + derive the output dataset. In this case, this pipeline is declaring that it
        starts from `User` and `Transaction` datasets.
      -* Notice the signature of the pipeline function in line 14 - it takes 2 arguments 
      -besides `cls` - they are essentially symbols for `User` dataset and `Transaction` 
      +* Notice the signature of the pipeline function in line 14 - it takes 2 arguments
      +besides `cls` - they are essentially symbols for `User` dataset and `Transaction`
       dataset respectively.
      -* Pipeline's function body is able to manipulate these symbols and create other dataset 
      -objects. For instance, line 15 joins these two datasets and the resulting dataset 
      -is stored in variable `joined`. Line 16 does a `filter` operation on `joined` and 
      -stores the result in another dataset called `abroad`. Finally lines 19-32 
      -aggregate the `abroad` dataset and create a dataset matching the schema defined 
      +* Pipeline's function body is able to manipulate these symbols and create other dataset
      +objects. For instance, line 15 joins these two datasets and the resulting dataset
      +is stored in variable `joined`. Line 16 does a `filter` operation on `joined` and
      +stores the result in another dataset called `abroad`. Finally lines 19-32
      +aggregate the `abroad` dataset and create a dataset matching the schema defined
       in lines 1-9.
       
       That's it - your first Fennel pipeline! Now let's look at a few more related
      @@ -50,20 +50,20 @@ ideas.
       ### Operators
       
       Fennel pipelines are built out of a few general purpose operators like `filter`,
      -`transform`, `join` etc which can be composed together to write any pipeline. 
      +`transform`, `join` etc which can be composed together to write any pipeline.
       You can read about all the operators [here](/concepts/pipeline#operators). Further,
      -a few operators (e.g. `transform`, `filter`) take free-form Python using which 
      -arbitrary computation can be done (including making calls into external services 
      -if needed). For all such operators, input/outputs variables are Pandas Dataframes
      +a few operators (e.g. `transform`, `filter`) take free-form Python using which
      +arbitrary computation can be done (including making calls into external services
      +if needed). For all such operators, input/outputs variables are Pandas DataFrames
       or Pandas Series. Here is an example with `transform` operator demonstrating this:
       
       
      
       
       
      -The ONLY constraint on the pipeline topology is that `aggregate` has to be the 
      +The ONLY constraint on the pipeline topology is that `aggregate` has to be the
       terminal node i.e. it's not allowed to compose any other operator on the output
      -of `aggregate` operator. This constraint allows Fennel to 
      -significantly reduce costs/perf of pipelines. And it's possible that even this
      +of `aggregate` operator. This constraint allows Fennel to
      +significantly reduce costs/performance of pipelines. And it's possible that even this
       constraint will be removed in the future.
       
       
      @@ -71,15 +71,15 @@ constraint will be removed in the future.
       
       Fennel pipelines have a bunch of really desirable properties:
       
      -1. **Extremely declrative** - you don't need to specifiy where/how pipelines should
      +1. **Extremely declarative** - you don't need to specify where/how pipelines should
           run, how much RAM they should take, how should they be partitioned if datasets
      -    are too big etc. 
      +    are too big etc.
       
      -2. **Python Native** - as mentioned above, it's possible to run arbitrary Python 
      -   computation in pipelines. You can even import your favorite packages to use 
      +2. **Python Native** - as mentioned above, it's possible to run arbitrary Python
      +   computation in pipelines. You can even import your favorite packages to use
          and/or make external API calls.
       
      -3. **Realtime** - as soon as new data arrives in any input dataset, pipeline 
      +3. **Realtime** - as soon as new data arrives in any input dataset, pipeline
          propagates the derived data downstream. The same mechanism works whether
          the input dataset is continuously getting data in realtime or if it gets data
          in a batch fashion. The pipeline code is same in both realtime and batch cases.
      @@ -88,5 +88,5 @@ Fennel pipelines have a bunch of really desirable properties:
          declaring the intent to make changes. As a result, situations where half the data
          was derived using an older undocumented version of code never happen.
       
      -5. **Auto-backfilled** - pipelines are backfilled automatically on declaration. There 
      -   is no separate backfill operation. 
      +5. **Auto-backfilled** - pipelines are backfilled automatically on declaration. There
      +   is no separate backfill operation.
      diff --git a/docs/pages/concepts/source.md b/docs/pages/concepts/source.md
      index 514df1259..70219dcf7 100644
      --- a/docs/pages/concepts/source.md
      +++ b/docs/pages/concepts/source.md
      @@ -10,7 +10,7 @@ Source is the ONLY way to get data into Fennel. There are two ways to source dat
       
       You can either log data into Fennel using a Webhook source or you can source data from your external datastores.
       
      -Fennel ships with data connectors to all [common datastores](/api-reference/sources) so that you can 
      +Fennel ships with data connectors to all [common datastores](/api-reference/sources) so that you can
       'source' your Fennel datasets from your external datasets. Let's see an example:
       
       ### **Example**
      @@ -27,67 +27,67 @@ class UserLocation:
           uid: int
           city: str
           country: str
      -    update_time: datetime    
      +    update_time: datetime
       ```
       
      -In this example, line 3 creates an object that knows how to connect with your 
      -Postgres database. Line 6-12 describe a dataset that needs to be sourced from 
      -the Postgres. And line 5 declares that this dataset should be sourced from a 
      -table named `user` within the Postgres database. And that's it - once this 
      -is written, `UserLocation` dataset will start mirroring your postgres table 
      +In this example, line 3 creates an object that knows how to connect with your
      +Postgres database. Line 6-12 describe a dataset that needs to be sourced from
      +the Postgres. And line 5 declares that this dataset should be sourced from a
      +table named `user` within the Postgres database. And that's it - once this
      +is written, `UserLocation` dataset will start mirroring your postgres table
       `user`and will update as the underlying Postgres table updates. 
       
       Most sources take a few additional parameters as described below:
       
      -### Every 
      -The frequency with which Fennel checks the external data source for new data. 
      +### Every
      +The frequency with which Fennel checks the external data source for new data.
       Needed for all sources except Kafka and Webhooks which are ingested continuously.
       
       ### Cursor
      -Fennel uses a cursor to do incremental ingestion of data. It does so 
      -by remembering the last value of the cursor column (in this case `update_time`) 
      +Fennel uses a cursor to do incremental ingestion of data. It does so
      +by remembering the last value of the cursor column (in this case `update_time`)
       and issuing a query of the form `SELECT * FROM user WHERE update_time > {last_update_time}`.
      -Clearly, this works only when the cursor field is monotonically increasing with 
      -row updates - which Fennel expects you to ensure. It is also advised to have 
      -an index of the cursor column so that this query is efficient. All data sources 
      +Clearly, this works only when the cursor field is monotonically increasing with
      +row updates - which Fennel expects you to ensure. It is also advised to have
      +an index of the cursor column so that this query is efficient. All data sources
       except Kafka, Webhooks & S3 require a cursor.
       
       ### Lateness
       Fennel, like many other streaming systems, is designed to robustly handle out
       of order data. If there are no bounds on how out of order data can get, the state
      -can blow up. Unlike some other systems, Fennel keeps this state on disk which 
      -eliminates OOM issues. But even then, it's desirable to garbaget collect this state
      -when all data before a timestamp has been seen. 
      -
      -This is ususally handled by a technique called [Watermarking](https://www.oreilly.com/radar/the-world-beyond-batch-streaming-102/) 
      -where max out of order delay is specified. This max out of order delay of a source 
      -is called `lateness` in Fennel, and once specified at source level, is respected 
      -automatically by each downstream pipeline. In this example, by setting `lateness` 
      -as `1d`, we are telling Fennel that once it sees a data with timestamp `t`, it 
      +can blow up. Unlike some other systems, Fennel keeps this state on disk which
      +eliminates OOM issues. But even then, it's desirable to garbage collect this state
      +when all data before a timestamp has been seen.
      +
      +This is usually handled by a technique called [Watermarking](https://www.oreilly.com/radar/the-world-beyond-batch-streaming-102/)
      +where max out of order delay is specified. This max out of order delay of a source
      +is called `lateness` in Fennel, and once specified at source level, is respected
      +automatically by each downstream pipeline. In this example, by setting `lateness`
      +as `1d`, we are telling Fennel that once it sees a data with timestamp `t`, it
       will never see data with timestamp older than `t-1 day` and if it does see older
       data, it's free to discard it.
       
       
       ## Schema Matching
       
      -Once Fennel obtains data from a source (usually as json string), the data needs to 
      -be parsed to extract and validate all the schema fields. Fennel expects the names 
      -of the fields in the dataset to match the schema of ingested json string. In this 
      -example, it is expected that the `user` table in Postgres will have at leat four 
      -columns -`uid`, `city`, `country`, and `update_time` with appropriate types. Note 
      +Once Fennel obtains data from a source (usually as json string), the data needs to
      +be parsed to extract and validate all the schema fields. Fennel expects the names
      +of the fields in the dataset to match the schema of ingested json string. In this
      +example, it is expected that the `user` table in Postgres will have at least four
      +columns -`uid`, `city`, `country`, and `update_time` with appropriate types. Note
       that the postgres table could have many more columns too - they are simply ignored.
      -If ingested data doesn't match with the schema of the Fennel dataset, the data 
      +If ingested data doesn't match with the schema of the Fennel dataset, the data
       is discarded and not admitted to the dataset. Fennel maintains logs of how often
       it happens and it's possible to set alerts on that.
       
       Here is how various types are matched from the sourced data:
       
       * `int`, `float`, `str`, `bool` respectively match with any integer types, float
      - types, string types and boolean types. For instance, Fennel's `int` type 
      + types, string types and boolean types. For instance, Fennel's `int` type
        matches with INT8 or UINT32 from Postgres.
       * `List[T]` matches a list of data of type T.
       * `Dict[T]` matches any dictionary from strings to values of type T.
      -* `Option[T]` matches if either the value is `null` or if its non-null value 
      +* `Option[T]` matches if either the value is `null` or if its non-null value
         matches type T. Note that `null` is an invalid value for any non-Option types.
       * `datetime` matching is a bit more flexible to support multiple common
         data formats to avoid bugs due to incompatible formats. Fennel is able to
      @@ -96,33 +96,33 @@ Here is how various types are matched from the sourced data:
       ### Datetime Formats
       
       * Integers that describe timestamp as interval from Unix epoch e.g. `1682099757`
      - Fennel is smart enough to automatically deduce if an integer is describing 
      - timestamp as seconds, miliseconds, microseconds or nanoseconds
      + Fennel is smart enough to automatically deduce if an integer is describing
      + timestamp as seconds, milliseconds, microseconds or nanoseconds
       * Strings that are decimal representation of an interval from Unix epoch e.g.`"1682099757"`
      -* Strings describing timestamp in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) 
      +* Strings describing timestamp in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt)
         format e.g. `'2002-10-02T10:00:00-05:00'` or `'2002-10-02T15:00:00Z'` or `'2002-10-02T15:00:00.05Z'`
      -* Strings describing timestamp in [RFC2822](https://www.ietf.org/rfc/rfc2822.txt) 
      +* Strings describing timestamp in [RFC2822](https://www.ietf.org/rfc/rfc2822.txt)
        format e.g. `'Sun, 23 Jan 2000 01:23:45 JST'`
       
       ### Safety of Credentials
       
      -In the above example, the credentials are defined in the code itself, which 
      -usually is not a good practice from a security point of view. Instead, Fennel 
      +In the above example, the credentials are defined in the code itself, which
      +usually is not a good practice from a security point of view. Instead, Fennel
       recommends two ways of using Sources securely:
       
       1. Using environment variables on your local machines
      -2. Defining credentials in Fennel's web console and referring to sources by 
      +2. Defining credentials in Fennel's web console and referring to sources by
          their names in the Python definitions.
       
      -In either approach, once the credentials reach the Fennel servers, they are 
      +In either approach, once the credentials reach the Fennel servers, they are
       securely stored in a Secret Manager and/or encrypted disks.
       
       ### Load Impact of Sources
       
       Fennel sources have negligible load impact on the external data sources. For instance,
      -in the above example, as long as indices are put on the cursor field, Fennel will 
      -make a single SELECT query on Postgres every minute. And once data reaches Fennel 
      -datasets, all subsequent operations are done using the copy of the data stored on 
      +in the above example, as long as indices are put on the cursor field, Fennel will
      +make a single SELECT query on Postgres every minute. And once data reaches Fennel
      +datasets, all subsequent operations are done using the copy of the data stored on
       Fennel servers, not the underlying data sources. This ensures that external data
       sources never need to be over-provisioned (or changed in any way) just for Fennel
       to be able to read the data.
      diff --git a/docs/pages/data-quality/lineage-validation.md b/docs/pages/data-quality/lineage-validation.md
      index f171da22c..d49a1e1b4 100644
      --- a/docs/pages/data-quality/lineage-validation.md
      +++ b/docs/pages/data-quality/lineage-validation.md
      @@ -6,10 +6,10 @@ status: 'published'
       
       # Lineage Validation
       
      -Fennel has visibility into the full lineage of dataflow graph. During `sync` 
      +Fennel has visibility into the full lineage of dataflow graph. During `sync`
       call, Fennel conducts various validations across the full lineage graph to make
      -sure that the dataflow graph is valid. Sync call succeeds if and only if 
      -all these validations pass at the compile time itself. These checks prevent 
      +sure that the dataflow graph is valid. Sync call succeeds if and only if
      +all these validations pass at the compile time itself. These checks prevent
       data quality bugs of the following kind:
       
       
      @@ -17,26 +17,26 @@ data quality bugs of the following kind:
       
       If any construct depends on something else (e.g. a pipeline takes another dataset
       as an input), Fennel validates that all such dependencies are available. This means
      -that a construct can not be deleted until everything that depends on it has been 
      +that a construct can not be deleted until everything that depends on it has been
       deleted first.
       
       When a feature depends on an upstream pipeline (possibly via multiple dependency hops),
       deletion of that pipeline can lead to values of that dataset becoming nulls. This changes
       the distribution of feature values leading to incorrect model results. But such
      -scenarios can not occur with Fennel because this missing dependency will be caught at 
      +scenarios can not occur with Fennel because this missing dependency will be caught at
       "compile time" itself
       
       
       **Typing Mismatch**
       
      -Fennel matches data types across depencies to detect invalid relationships. 
      +Fennel matches data types across dependencies to detect invalid relationships.
       For instance, if a dataset is supposed to have a field of certain type but the pipeline that produces
       the dataset doesn't produce that field/type, the error will be caught during `sync`
       only without ever going into the runtime phase.
       
       
      -**Circular Depencies**
      +**Circular Dependencies**
       
      -Fennel is also able to detect circular dependencies in dataflow during sync. 
      +Fennel is also able to detect circular dependencies in dataflow during sync.
       While these aren't that common in production, but when they do happen, can be
      -very hard to debug.
      \ No newline at end of file
      +very hard to debug.
      diff --git a/docs/pages/data-quality/metaflags.md b/docs/pages/data-quality/metaflags.md
      index 17dfac2e5..1f55f9e18 100644
      --- a/docs/pages/data-quality/metaflags.md
      +++ b/docs/pages/data-quality/metaflags.md
      @@ -12,13 +12,13 @@ objects.
       
       Here are a few common scenarios where Metaflags help:
       
      -* Ownership of a dataset needs to be tracked so that if it is having data 
      - quality issues, the problem can be routed to an appropriate person to 
      +* Ownership of a dataset needs to be tracked so that if it is having data
      + quality issues, the problem can be routed to an appropriate person to
        investigate.
       * Features and data need to be documented so that their users can easily
         understand what they are doing. 
       * Due to compliance reasons, all features that depend on PII data either directly
      -  or through a long list of upstream dependencies need to be audited - but for 
      +  or through a long list of upstream dependencies need to be audited - but for
         that, first all such features need to be identified. 
       
       Let's look at an example:
      @@ -37,10 +37,10 @@ class User:
       class UserFeatures:
           uid: int = feature(id=1)
           zip: str = feature(id=2).meta(tags=['PII'])
      -    bmi: float = feature(id=3).meta(owner='alan@xyz.ai')        
      -    bmr: float = feature(id=4).meta(deperecated=True)
      +    bmi: float = feature(id=3).meta(owner='alan@xyz.ai')
      +    bmr: float = feature(id=4).meta(deprecated=True)
           ..
      -    
      +
           @meta(description='based on algorithm specified here: bit.ly/xyy123')
           @extractor
           @inputs(...)
      @@ -51,39 +51,39 @@ class UserFeatures:
       
       Fennel currently supports 5 metaflags:
       
      -1. **owner** - email address of the owner of the object. The ownership flows down transitively. 
      +1. **owner** - email address of the owner of the object. The ownership flows down transitively.
          For instance, the owner of a featureset becomes the default owner of all the features unless
           it is explicitly overwritten by specifying an owner for that feature. 
       2. **description** - description of the object, used solely for documentation purposes.
      -3. **tags** - list of arbitrary string tags associated with the object. Tags flow across the 
      -   lineage graph and are additive. For instance, if a dataset is tagged with tag 'PII', all 
      -   other objects that read from the dataset will inherit this tag. Fennel supports searching 
      +3. **tags** - list of arbitrary string tags associated with the object. Tags flow across the
      +   lineage graph and are additive. For instance, if a dataset is tagged with tag 'PII', all
      +   other objects that read from the dataset will inherit this tag. Fennel supports searching
          for objects with a given tag. 
      -4. **deleted** - whether the object is deleted or not. Sometimes it is desirable to delete 
      +4. **deleted** - whether the object is deleted or not. Sometimes it is desirable to delete
          the object but keep a marker tombstone in the codebase - that is where deleted should be
           used. For instance, maybe a feature is now deleted but its ID should not be reused again.
           It'd be a good idea to mark it as deleted and leave it like that forever (the code for
          its extractor can be removed)
      -5. **deprecated** - same as deleted but just marks the object as to be deprecated in the near 
      +5. **deprecated** - same as deleted but just marks the object as to be deprecated in the near
          future. If an object uses a deprecated object, the owner will get periodic reminders to modify
      -    their object to not dependon the deprecated object any more. 
      +    their object to not depend on the deprecated object any more. 
       
       
       ### Enforcement of Ownership
       
       Enforcing ownership of code is a well known approach in software engineering
      -to maintain the quality & health of code but most ML teams don't enforce 
      +to maintain the quality & health of code but most ML teams don't enforce
       ownership of pipelines or features.
       
      -Fennel requires that every dataset and feature has an explicit owner email and 
      +Fennel requires that every dataset and feature has an explicit owner email and
       routes alerts/notifications about those constructs to the owners. As people
      -change teams or move around, this makes it more likely that context will be 
      +change teams or move around, this makes it more likely that context will be
       transferred.
       
      -Fennel also makes it easy to identify downstream dependencies - e.g. given a 
      -dataset, it's easy to see if any other datasets or features depend on it. 
      -Knowing that a construct has truly no dependencies makes it that much easier 
      +Fennel also makes it easy to identify downstream dependencies - e.g. given a
      +dataset, it's easy to see if any other datasets or features depend on it.
      +Knowing that a construct has truly no dependencies makes it that much easier
       for teams to simply delete them on ownership transitions vs keeping them around.
       
      -Ownership and other metaflags in itself don't magically prevent any quality 
      -issues but hopefully should lead to subjectively higher hygiene for code and data.
      \ No newline at end of file
      +Ownership and other metaflags in itself don't magically prevent any quality
      +issues but hopefully should lead to subjectively higher hygiene for code and data.
      diff --git a/docs/pages/data-quality/strong-typing.md b/docs/pages/data-quality/strong-typing.md
      index 56fe5fbb4..a9fc81483 100644
      --- a/docs/pages/data-quality/strong-typing.md
      +++ b/docs/pages/data-quality/strong-typing.md
      @@ -6,13 +6,13 @@ status: 'published'
       
       # Strong Typing
       
      -Fennel supports a rich and powerful [data type system](/api-reference/data-types). All 
      -dataset fields and features in Fennel must be given a type and Fennel enforces these 
      +Fennel supports a rich and powerful [data type system](/api-reference/data-types). All
      +dataset fields and features in Fennel must be given a type and Fennel enforces these
       types strongly. In particular, types don't auto typecast (e.g.
       `int` values can not be passed where `float` is expected) and nullable types
       are explicitly declared (e.g. `Optional[str]` can take nulls but not `str`).
       
      -Let's see how this helps precent quality bugs:
      +Let's see how this helps prevent quality bugs:
       
       **Dataset Fields**
       
      @@ -29,7 +29,7 @@ than what is supported by programming languages. For instance, if a dataset fiel
       represents a zip code, while the datatype is `str`, only a subset of strings that
       match a zip code regex are semantically valid. Or as another example, if a dataset
       field represents gender, maybe only a handful of values are valid (e.g. `male`, `female`,
      -`non-binary` etc.). Fennel's type system supports [type restrictions](/api-reference/data-types) 
      +`non-binary` etc.). Fennel's type system supports [type restrictions](/api-reference/data-types)
       using which all these and lot more constraints can be encoded as data types and thus get checked
       at compile and runtime everywhere.
       
      @@ -39,6 +39,6 @@ at compile and runtime everywhere.
       Timestamps can be encoded in a variety of formats and this often creates a bunch of
       bugs in data engineering world. Fennel has a separate data type for `datetime` which
       is automatically parsed from a wide variety of formats. As a result, some of the data
      -may be encodeing time as milliseconds since epoch and another as a string in RFC 3339 
      +may be encoding time as milliseconds since epoch and another as a string in RFC 3339
       format and Fennel supports their inter-operation quite nicely.
       
      diff --git a/docs/pages/development/monitoring-alerting.md b/docs/pages/development/monitoring-alerting.md
      index d7d20d442..bb96e9e82 100644
      --- a/docs/pages/development/monitoring-alerting.md
      +++ b/docs/pages/development/monitoring-alerting.md
      @@ -9,25 +9,25 @@ status: 'published'
       Fennel is inspired by the Unix philosophy of doing a few things
       really well and seamlessly composing with other tools for the rest.
       
      -For monitoring/alerting, Fennel exposes all relevant metrics behind a Prometheus 
      +For monitoring/alerting, Fennel exposes all relevant metrics behind a Prometheus
       endpoint. You can point Grafana, Datadog, or any other metric system that speaks
      -Prometheus protocol towards this endpoint. Once your metric system is 
      -connected to Fennel's Prometheus endpoint, you can seamlessly use your existing 
      -monitoring/alerting stack. 
      +the Prometheus protocol towards this endpoint. Once your metric system is
      +connected to Fennel's Prometheus endpoint, you can seamlessly use your existing
      +monitoring/alerting stack.
       
       
      -## Incident Management & Pagerduty
      -Since Fennel is completely managed, you don't need to be on Pager Duty for incidents
      -affecting Fennel system itself - Fennel engineers get paged for those. 
      +## Incident Management & PagerDuty
      +Since Fennel is completely managed, you don't need to be on PagerDuty for incidents
      +affecting the Fennel system itself - Fennel engineers get paged for those.
       
      -However, if you wanted to set your own pager duty alerting for application metrics, 
      -you can do so on your own on top of the metrics exposed behind the Prometheus 
      +However, if you wanted to set your own PagerDuty alerting for application metrics,
      +you can do so on your own on top of the metrics exposed behind the Prometheus
       endpoint.
       
       
       ## Exposed Metrics
       
      -Some examples of metrics that are avaialble behind this Prometheus endpoint:
      +Some examples of metrics that are available behind this Prometheus endpoint:
       - Count & latencies of all API calls
       - Lag of pipelines and data sources
       - Error counts, along both read and write paths
      diff --git a/docs/pages/getting-started/quickstart.md b/docs/pages/getting-started/quickstart.md
      index e8432a05c..4392fa239 100644
      --- a/docs/pages/getting-started/quickstart.md
      +++ b/docs/pages/getting-started/quickstart.md
      @@ -5,7 +5,7 @@ status: 'published'
       ---
       # Quickstart
       
      -The following example tries to show how several concepts in Fennel come together to solve a problem.  
      +The following example tries to show how several concepts in Fennel come together to solve a problem.
       
       ### 0. Installation
       We only need to install Fennel's Python client to run this example, so let's install that first:
      @@ -13,7 +13,7 @@ We only need to install Fennel's Python client to run this example, so let's ins
       pip install fennel-ai
       ```
       
      -And while we are at it, let's add all the imports that we will need in the 
      +And while we are at it, let's add all the imports that we will need in the
       rest of the tutorial:
       
       
      
      @@ -21,49 +21,49 @@ rest of the tutorial:
       
       ### 1. Data Connectors
       
      -Fennel ships with data connectors that know how to talk to all common data 
      -sources. The connectors can be defined in code or in Fennel console (not shown 
      +Fennel ships with data connectors that know how to talk to all common data
      +sources. The connectors can be defined in code or in Fennel console (not shown
       here).
       
      
       
       ### 2. Datasets
      -Datasets are the tables that you want to use in your feature pipelines. These 
      -are constantly kept fresh as new data arrives from connectors. 
      +Datasets are the tables that you want to use in your feature pipelines. These
      +are constantly kept fresh as new data arrives from connectors.
       
      
       
      -Fennel also lets you derive more datasets by defining pipelines that transform 
      -data across different sources (e.g. s3, kafka, postgres etc.) in the same plane 
      -of abstraction.  These pipelines are highly declarative, completely Python native, 
      +Fennel also lets you derive more datasets by defining pipelines that transform
      +data across different sources (e.g. s3, kafka, postgres etc.) in the same plane
      +of abstraction.  These pipelines are highly declarative, completely Python native,
       realtime, versioned, are auto backfilled on declaration, and can be unit tested.
       
      
       
       ### 3. Featuresets
      -Featuresets are containers for the features that you want to extract from your 
      -datasets. Features, unlike datasets, have no state and are computed on the 
      -"read path" (i.e. when you query for them) via arbitrary Python code. Features 
      -are immutable to improve reliability.  
      +Featuresets are containers for the features that you want to extract from your
      +datasets. Features, unlike datasets, have no state and are computed on the
      +"read path" (i.e. when you query for them) via arbitrary Python code. Features
      +are immutable to improve reliability.
       
      
       
       
       
       ### 4. Sync
      -Once datasets/featurests have been written (or updated), you can sync those 
      -definitions with the server by instantiating a client and using it to talk to 
      +Once datasets/featuresets have been written (or updated), you can sync those
      +definitions with the server by instantiating a client and using it to talk to
       server.
      -Since we are not working with a real server, here we use the MockClient to run 
      +Since we are not working with a real server, here we use the MockClient to run
       this example locally instead of a real client. Mock Client doesn't support data
       connectors so we will manually log some data to simulate data flows.
       
      
       
       ### 5. Query
      -This is the read path of Fennel. You can query for live features (i.e. features 
      -using the latest value of all datasets) like this: 
      +This is the read path of Fennel. You can query for live features (i.e. features
      +using the latest value of all datasets) like this:
       
      
       
      -You can also query for historical values of features at aribtrary timestamps (
      +You can also query for historical values of features at arbitrary timestamps (
       useful in creating training datasets) like this:
       
       
      
       
       Query requests can be made over REST API from any language/tool which makes it easy
      -to ship features to production servers.
      \ No newline at end of file
      +to ship features to production servers.
      diff --git a/docs/pages/guides/lambda-architecture.md b/docs/pages/guides/lambda-architecture.md
      index fe0afe97e..8996390b1 100644
      --- a/docs/pages/guides/lambda-architecture.md
      +++ b/docs/pages/guides/lambda-architecture.md
      @@ -35,8 +35,8 @@ Here imagine that we have two different datasets, potentially with their own sep
       
       ### **Implementing Lambda Architecture Via Multiple Pipelines**
       
      -Generally speaking, Fennel itself follows [Kappa architecture](https://www.kai-waehner.de/blog/2021/09/23/real-time-kappa-architecture-mainstream-replacing-batch-lambda/) and expresses all computation via streaming. But it can be trivially used to implement [lambda architecture](https://www.databricks.com/glossary/lambda-architecture) for your usecases. 
      +Generally speaking, Fennel itself follows [Kappa architecture](https://www.kai-waehner.de/blog/2021/09/23/real-time-kappa-architecture-mainstream-replacing-batch-lambda/) and expresses all computation via streaming. But it can be trivially used to implement [lambda architecture](https://www.databricks.com/glossary/lambda-architecture) for your use cases. 
       
       Imagine you have a realtime source (say in Kafka) and a batch source (say in Snowflake) that is batch corrected every night. And you'd like to do some computation using Kafka in realtime but also correct the data later when Snowflake data is available. That can be trivially done by writing a dataset having two pipelines - one of them can build on top of Kafka and work realtime. The other one can build on Snowflake and process the same data later. 
       
      -In this way, batch corrected data is put in the destination dataset later, and hence ends up "overwriting" the earlier realtime data from Kafka pipeline, hence giving you the full power of lambda architecture. 
      \ No newline at end of file
      +In this way, batch corrected data is put in the destination dataset later, and hence ends up "overwriting" the earlier realtime data from Kafka pipeline, hence giving you the full power of lambda architecture. 
      diff --git a/docs/pages/guides/request-based-features.md b/docs/pages/guides/request-based-features.md
      index 0ee9b2a7f..71fd55cb2 100644
      --- a/docs/pages/guides/request-based-features.md
      +++ b/docs/pages/guides/request-based-features.md
      @@ -24,22 +24,22 @@ class UserFeatures:
           ...
           ctr_by_device_type: float = feature(id=17)
           ..
      -    
      +
           @extractor
           @inputs(SearchRequest.device_type)
           @outputs(ctr_by_device_type)
      -    def f(cls, ts: pd.Series, devices: pd.Series): 
      +    def f(cls, ts: pd.Series, devices: pd.Series):
               for device in devices:
                   ...
       
       ```
       
      -In this example, we defined a featureset called `SearchRequest` that contains 
      +In this example, we defined a featureset called `SearchRequest` that contains
       all the properties of the request that are relevant for a feature. This featureset
       itself has no extractors - Fennel doesn't know how to extract any of these features.
       
      -Features of other featuresets can now depend on `SearchRequest` features - in this 
      -example some features of `UserFeatures` are depending on `SearchRequest.device_type.` 
      +Features of other featuresets can now depend on `SearchRequest` features - in this
      +example some features of `UserFeatures` are depending on `SearchRequest.device_type.`
       This way, as long as `SearchRequest` features are passed as input to extraction
       process, all such other features can be naturally computed.
       
      diff --git a/docs/pages/misc/troubleshooting-guide.md b/docs/pages/misc/troubleshooting-guide.md
      index 1483c11bc..91ffe0dd6 100644
      --- a/docs/pages/misc/troubleshooting-guide.md
      +++ b/docs/pages/misc/troubleshooting-guide.md
      @@ -13,7 +13,7 @@ status: 'published'
       Some users have reported that they could not connect to Amazon RDS MySQL or MariaDB. This can be diagnosed with the error message: `Cannot create a PoolableConnectionFactory`. To solve this issue please set **`jdbc_params` ** to **** `enabledTLSProtocols=TLSv1.2` 
       
       
      -
      +
      @@ -28,7 +28,7 @@ print(filtered_ds.schema())
      -
      +
      @@ -36,12 +36,12 @@ print(filtered_ds.schema()) It might be helpful to print the current data that is stored in the datasets that you are using to extract features. You can do this by calling : -`client.get_dataset_df(dataset_name) ` and printing the resultant dataframe. +`client.get_dataset_df(dataset_name) ` and printing the resultant dataframe. Please note that this debug functionality is only available in the mock client. To debug issues in prod, you need to use -the metadata api's exposed by Fennel. +the metadata APIs exposed by Fennel. Another possibility is that the timestamps for the datasets are equal to or ahead of the current time. Since fennel ensures that everything is point in time correct, this can result in null values for the features. -
      \ No newline at end of file + From a4ef20a7a54b170b94068ff1c475385e58e5af73 Mon Sep 17 00:00:00 2001 From: David WF Date: Mon, 21 Aug 2023 16:25:15 -0700 Subject: [PATCH 2/7] Fix workflow definition --- .github/workflows/spellcheck.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index d4dd922aa..a40ecc280 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,7 +1,5 @@ name: Spellcheck - on: [pull_request, push] - jobs: checks: runs-on: ubuntu-latest @@ -10,5 +8,4 @@ jobs: - name: Check out source repository uses: actions/checkout@v3 - uses: rojopolis/spellcheck-github-actions@v0 - working-directory: docs/ name: Spellcheck From fee2f92737681c138d828b3dc542bab0a36fc143 Mon Sep 17 00:00:00 2001 From: David WF Date: Mon, 21 Aug 2023 16:31:37 -0700 Subject: [PATCH 3/7] Refactor to make github actions better --- .github/workflows/spellcheck.yml | 2 +- .gitignore | 2 +- docs/.pyspelling.yml => .pyspelling.yml | 6 +++--- docs/.wordlist.txt => .wordlist.txt | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename docs/.pyspelling.yml => .pyspelling.yml (86%) rename docs/.wordlist.txt => .wordlist.txt (100%) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index a40ecc280..d67ca0e89 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,5 +1,5 @@ name: Spellcheck -on: [pull_request, push] +on: [pull_request] jobs: checks: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 32a2f8d50..c6d761aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,4 @@ dmypy.json /data/*.csv **/.DS_Store - +wordlist.dic diff --git a/docs/.pyspelling.yml b/.pyspelling.yml similarity index 86% rename from docs/.pyspelling.yml rename to .pyspelling.yml index 5bffc1505..f416ca51d 100644 --- a/docs/.pyspelling.yml +++ b/.pyspelling.yml @@ -1,17 +1,17 @@ matrix: - name: Python source sources: - - examples/**/*.py + - docs/examples/**/*.py dictionary: wordlists: - .wordlist.txt - output: wordlist.dic + output: docs/wordlist.dic encoding: utf-8 pipeline: - pyspelling.filters.python: - name: markdown sources: - - '**/*.md' + - 'docs/**/*.md' dictionary: wordlists: - .wordlist.txt diff --git a/docs/.wordlist.txt b/.wordlist.txt similarity index 100% rename from docs/.wordlist.txt rename to .wordlist.txt From 454be1368462fd76c4cd60d54a268a8aa3474333 Mon Sep 17 00:00:00 2001 From: David WF Date: Mon, 21 Aug 2023 16:35:38 -0700 Subject: [PATCH 4/7] Fix spelling errors and add new words to wordlist --- .wordlist.txt | 5 +++++ docs/pages/api-reference/sources.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.wordlist.txt b/.wordlist.txt index f31498154..28045233b 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -32,7 +32,9 @@ MockClient Nones OAuth OOM +OpenSSL's PII +PLAINTEXT PagerDuty PoolableConnectionFactory PrivateLink @@ -44,8 +46,11 @@ ROI RPCs Realtimeliness RocksDB +SASL SDK +SHA SLA +SSL SearchRequest Signifier Stddev diff --git a/docs/pages/api-reference/sources.md b/docs/pages/api-reference/sources.md index 0b6156a42..ebbf1b938 100644 --- a/docs/pages/api-reference/sources.md +++ b/docs/pages/api-reference/sources.md @@ -174,6 +174,6 @@ The following fields need to be defined on the topic: ### Delta Lake -Similar to Hudi, Fennel integrates with Delta Lkae via its S3 connector. To use delta lake, simply set the `format` field to "delta" when configuring the S3 bucket. +Similar to Hudi, Fennel integrates with Delta Lake via its S3 connector. To use delta lake, simply set the `format` field to "delta" when configuring the S3 bucket.
      
      
      From a183a7b44ce180826c72eaa9e0a9df1ada209617 Mon Sep 17 00:00:00 2001
      From: David WF 
      Date: Mon, 21 Aug 2023 16:49:21 -0700
      Subject: [PATCH 5/7] Move spellcheck to linting
      
      ---
       .github/workflows/python_lint.yml |   8 +-
       poetry.lock                       | 373 ++++++++++++++++++------------
       pyproject.toml                    |   1 +
       3 files changed, 226 insertions(+), 156 deletions(-)
      
      diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml
      index c81ee05d4..91dd15dbb 100644
      --- a/.github/workflows/python_lint.yml
      +++ b/.github/workflows/python_lint.yml
      @@ -13,13 +13,17 @@ jobs:
               uses: actions/setup-python@v4
               with:
                 python-version: 3.11
      +      - name: Install aspell for pyspelling
      +        run: apt-get install -y aspell
             - name: Upgrade pip
               run: pip install --upgrade pip
             - name: Install packages
      -        run: pip install "flake8>=4.0.1" "black>=22.6.0" "mypy==0.981" # install 0.981 of mypy since future versions seem to be not working with `--exclude`
      +        run: pip install "flake8>=4.0.1" "black>=22.6.0" "mypy==0.981" "pyspelling>=2.8.2" # install 0.981 of mypy since future versions seem to be not working with `--exclude`
             - name: flake8 lint
               run: flake8 .
             - name: black lint
               run: black --diff --check .
             - name: mypy typechecking
      -        run: mypy .
      \ No newline at end of file
      +        run: mypy .
      +      - name: spellcheck
      +        run: pyspelling
      diff --git a/poetry.lock b/poetry.lock
      index c70817360..958f1faff 100644
      --- a/poetry.lock
      +++ b/poetry.lock
      @@ -1,10 +1,9 @@
      -# This file is automatically @generated by Poetry and should not be changed by hand.
      +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
       
       [[package]]
       name = "aiofiles"
       version = "22.1.0"
       description = "File support for asyncio."
      -category = "dev"
       optional = false
       python-versions = ">=3.7,<4.0"
       files = [
      @@ -16,7 +15,6 @@ files = [
       name = "aiosqlite"
       version = "0.19.0"
       description = "asyncio bridge to the standard sqlite3 module"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -32,7 +30,6 @@ docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"]
       name = "anyio"
       version = "3.7.1"
       description = "High level compatibility layer for multiple asynchronous event loop implementations"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -54,7 +51,6 @@ trio = ["trio (<0.22)"]
       name = "appnope"
       version = "0.1.3"
       description = "Disable App Nap on macOS >= 10.9"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -66,7 +62,6 @@ files = [
       name = "argon2-cffi"
       version = "21.3.0"
       description = "The secure Argon2 password hashing algorithm."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -86,7 +81,6 @@ tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"]
       name = "argon2-cffi-bindings"
       version = "21.2.0"
       description = "Low-level CFFI bindings for Argon2"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -124,7 +118,6 @@ tests = ["pytest"]
       name = "arrow"
       version = "1.2.3"
       description = "Better dates & times for Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -139,7 +132,6 @@ python-dateutil = ">=2.7.0"
       name = "asttokens"
       version = "2.2.1"
       description = "Annotate AST trees with source code positions"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -157,7 +149,6 @@ test = ["astroid", "pytest"]
       name = "attrs"
       version = "23.1.0"
       description = "Classes Without Boilerplate"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -176,7 +167,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
       name = "babel"
       version = "2.12.1"
       description = "Internationalization utilities"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -191,7 +181,6 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
       name = "backcall"
       version = "0.2.0"
       description = "Specifications for callback functions passed in to an API"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -203,7 +192,6 @@ files = [
       name = "beautifulsoup4"
       version = "4.12.2"
       description = "Screen-scraping library"
      -category = "dev"
       optional = false
       python-versions = ">=3.6.0"
       files = [
      @@ -222,7 +210,6 @@ lxml = ["lxml"]
       name = "black"
       version = "22.12.0"
       description = "The uncompromising code formatter."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -258,7 +245,6 @@ uvloop = ["uvloop (>=0.15.2)"]
       name = "bleach"
       version = "6.0.0"
       description = "An easy safelist-based HTML-sanitizing tool."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -273,11 +259,21 @@ webencodings = "*"
       [package.extras]
       css = ["tinycss2 (>=1.1.0,<1.2)"]
       
      +[[package]]
      +name = "bracex"
      +version = "2.3.post1"
      +description = "Bash style brace expander."
      +optional = false
      +python-versions = ">=3.7"
      +files = [
      +    {file = "bracex-2.3.post1-py3-none-any.whl", hash = "sha256:351b7f20d56fb9ea91f9b9e9e7664db466eb234188c175fd943f8f755c807e73"},
      +    {file = "bracex-2.3.post1.tar.gz", hash = "sha256:e7b23fc8b2cd06d3dec0692baabecb249dda94e06a617901ff03a6c56fd71693"},
      +]
      +
       [[package]]
       name = "build"
       version = "0.9.0"
       description = "A simple, correct PEP 517 build frontend"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -301,7 +297,6 @@ virtualenv = ["virtualenv (>=20.0.35)"]
       name = "certifi"
       version = "2023.5.7"
       description = "Python package for providing Mozilla's CA Bundle."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -313,7 +308,6 @@ files = [
       name = "cffi"
       version = "1.15.1"
       description = "Foreign Function Interface for Python calling C code."
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -390,7 +384,6 @@ pycparser = "*"
       name = "charset-normalizer"
       version = "3.1.0"
       description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
      -category = "dev"
       optional = false
       python-versions = ">=3.7.0"
       files = [
      @@ -475,7 +468,6 @@ files = [
       name = "click"
       version = "8.1.4"
       description = "Composable command line interface toolkit"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -490,7 +482,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
       name = "colorama"
       version = "0.4.6"
       description = "Cross-platform colored terminal text."
      -category = "dev"
       optional = false
       python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
       files = [
      @@ -502,7 +493,6 @@ files = [
       name = "comm"
       version = "0.1.3"
       description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -522,7 +512,6 @@ typing = ["mypy (>=0.990)"]
       name = "cryptography"
       version = "41.0.1"
       description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -564,7 +553,6 @@ test-randomorder = ["pytest-randomly"]
       name = "debugpy"
       version = "1.6.7"
       description = "An implementation of the Debug Adapter Protocol for Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -592,7 +580,6 @@ files = [
       name = "decorator"
       version = "5.1.1"
       description = "Decorators for Humans"
      -category = "dev"
       optional = false
       python-versions = ">=3.5"
       files = [
      @@ -604,7 +591,6 @@ files = [
       name = "defusedxml"
       version = "0.7.1"
       description = "XML bomb protection for Python stdlib modules"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
       files = [
      @@ -616,7 +602,6 @@ files = [
       name = "docutils"
       version = "0.20.1"
       description = "Docutils -- Python Documentation Utilities"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -628,7 +613,6 @@ files = [
       name = "exceptiongroup"
       version = "1.1.2"
       description = "Backport of PEP 654 (exception groups)"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -643,7 +627,6 @@ test = ["pytest (>=6)"]
       name = "executing"
       version = "1.2.0"
       description = "Get the currently executing AST node of a frame, and other information"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -658,7 +641,6 @@ tests = ["asttokens", "littleutils", "pytest", "rich"]
       name = "fastjsonschema"
       version = "2.17.1"
       description = "Fastest Python implementation of JSON schema"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -673,7 +655,6 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc
       name = "flake8"
       version = "4.0.1"
       description = "the modular source code checker: pep8 pyflakes and co"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -690,7 +671,6 @@ pyflakes = ">=2.4.0,<2.5.0"
       name = "fqdn"
       version = "1.5.1"
       description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4"
       files = [
      @@ -702,7 +682,6 @@ files = [
       name = "frozendict"
       version = "2.3.8"
       description = "A simple immutable dictionary"
      -category = "main"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -749,7 +728,6 @@ files = [
       name = "geographiclib"
       version = "2.0"
       description = "The geodesic routines from GeographicLib"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -761,7 +739,6 @@ files = [
       name = "geopy"
       version = "2.3.0"
       description = "Python Geocoding Toolbox"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -781,11 +758,31 @@ dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (<
       requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"]
       timezone = ["pytz"]
       
      +[[package]]
      +name = "html5lib"
      +version = "1.1"
      +description = "HTML parser based on the WHATWG HTML specification"
      +optional = false
      +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
      +files = [
      +    {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"},
      +    {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
      +]
      +
      +[package.dependencies]
      +six = ">=1.9"
      +webencodings = "*"
      +
      +[package.extras]
      +all = ["chardet (>=2.2)", "genshi", "lxml"]
      +chardet = ["chardet (>=2.2)"]
      +genshi = ["genshi"]
      +lxml = ["lxml"]
      +
       [[package]]
       name = "idna"
       version = "3.4"
       description = "Internationalized Domain Names in Applications (IDNA)"
      -category = "dev"
       optional = false
       python-versions = ">=3.5"
       files = [
      @@ -797,7 +794,6 @@ files = [
       name = "importlib-metadata"
       version = "6.7.0"
       description = "Read metadata from Python packages"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -817,7 +813,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
       name = "importlib-resources"
       version = "5.12.0"
       description = "Read resources from Python packages"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -836,7 +831,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec
       name = "iniconfig"
       version = "2.0.0"
       description = "brain-dead simple config-ini parsing"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -848,7 +842,6 @@ files = [
       name = "ipykernel"
       version = "6.24.0"
       description = "IPython Kernel for Jupyter"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -862,7 +855,7 @@ comm = ">=0.1.1"
       debugpy = ">=1.6.5"
       ipython = ">=7.23.1"
       jupyter-client = ">=6.1.12"
      -jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
      +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
       matplotlib-inline = ">=0.1"
       nest-asyncio = "*"
       packaging = "*"
      @@ -882,7 +875,6 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio"
       name = "ipython"
       version = "8.12.2"
       description = "IPython: Productive Interactive Computing"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -922,7 +914,6 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa
       name = "ipython-genutils"
       version = "0.2.0"
       description = "Vestigial utilities from IPython"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -934,7 +925,6 @@ files = [
       name = "isoduration"
       version = "20.11.0"
       description = "Operations with ISO 8601 durations"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -949,7 +939,6 @@ arrow = ">=0.15.0"
       name = "jaraco-classes"
       version = "3.2.3"
       description = "Utility functions for Python class constructs"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -968,7 +957,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec
       name = "jedi"
       version = "0.18.2"
       description = "An autocompletion tool for Python that can be used for text editors."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -988,7 +976,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
       name = "jeepney"
       version = "0.8.0"
       description = "Low-level, pure Python DBus protocol wrapper."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1004,7 +991,6 @@ trio = ["async_generator", "trio"]
       name = "jinja2"
       version = "3.1.2"
       description = "A very fast and expressive template engine."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1022,7 +1008,6 @@ i18n = ["Babel (>=2.7)"]
       name = "json5"
       version = "0.9.14"
       description = "A Python implementation of the JSON5 data format."
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -1037,7 +1022,6 @@ dev = ["hypothesis"]
       name = "jsonpointer"
       version = "2.4"
       description = "Identify specific nodes in a JSON document (RFC 6901)"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
       files = [
      @@ -1049,7 +1033,6 @@ files = [
       name = "jsonschema"
       version = "4.18.0"
       description = "An implementation of JSON Schema validation for Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1081,7 +1064,6 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-
       name = "jsonschema-specifications"
       version = "2023.6.1"
       description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1097,7 +1079,6 @@ referencing = ">=0.28.0"
       name = "jupyter-client"
       version = "8.3.0"
       description = "Jupyter protocol implementation and client libraries"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1107,7 +1088,7 @@ files = [
       
       [package.dependencies]
       importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""}
      -jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
      +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
       python-dateutil = ">=2.8.2"
       pyzmq = ">=23.0"
       tornado = ">=6.2"
      @@ -1121,7 +1102,6 @@ test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pyt
       name = "jupyter-core"
       version = "5.3.1"
       description = "Jupyter core package. A base package on which Jupyter projects rely."
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1142,7 +1122,6 @@ test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
       name = "jupyter-events"
       version = "0.6.3"
       description = "Jupyter Event System library"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1167,7 +1146,6 @@ test = ["click", "coverage", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=
       name = "jupyter-server"
       version = "2.7.0"
       description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1180,7 +1158,7 @@ anyio = ">=3.1.0"
       argon2-cffi = "*"
       jinja2 = "*"
       jupyter-client = ">=7.4.4"
      -jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
      +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
       jupyter-events = ">=0.6.0"
       jupyter-server-terminals = "*"
       nbconvert = ">=6.4.4"
      @@ -1204,7 +1182,6 @@ test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-sc
       name = "jupyter-server-fileid"
       version = "0.9.0"
       description = ""
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1224,7 +1201,6 @@ test = ["jupyter-server[test] (>=1.15,<3)", "pytest", "pytest-cov"]
       name = "jupyter-server-terminals"
       version = "0.4.4"
       description = "A Jupyter Server Extension Providing Terminals."
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1244,7 +1220,6 @@ test = ["coverage", "jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-cov",
       name = "jupyter-server-ydoc"
       version = "0.8.0"
       description = "A Jupyter Server Extension Providing Y Documents."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1264,7 +1239,6 @@ test = ["coverage", "jupyter-server[test] (>=2.0.0a0)", "pytest (>=7.0)", "pytes
       name = "jupyter-ydoc"
       version = "0.2.4"
       description = "Document structures for collaborative editing using Ypy"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1284,7 +1258,6 @@ test = ["pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)", "ypy-we
       name = "jupyterlab"
       version = "3.6.5"
       description = "JupyterLab computational environment"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1313,7 +1286,6 @@ test = ["check-manifest", "coverage", "jupyterlab-server[test]", "pre-commit", "
       name = "jupyterlab-pygments"
       version = "0.2.2"
       description = "Pygments theme using JupyterLab CSS variables"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1325,7 +1297,6 @@ files = [
       name = "jupyterlab-server"
       version = "2.23.0"
       description = "A set of server components for JupyterLab and JupyterLab like applications."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1352,7 +1323,6 @@ test = ["hatch", "ipykernel", "jupyterlab-server[openapi]", "openapi-spec-valida
       name = "keyring"
       version = "24.2.0"
       description = "Store and access your passwords safely."
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1373,11 +1343,135 @@ completion = ["shtab"]
       docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
       testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
       
      +[[package]]
      +name = "lxml"
      +version = "4.9.3"
      +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
      +optional = false
      +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
      +files = [
      +    {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"},
      +    {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"},
      +    {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"},
      +    {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"},
      +    {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"},
      +    {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"},
      +    {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"},
      +    {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
      +    {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
      +    {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
      +    {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
      +    {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
      +    {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
      +    {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"},
      +    {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"},
      +    {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
      +    {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
      +    {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
      +    {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
      +    {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
      +    {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
      +    {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
      +    {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
      +    {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
      +    {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
      +    {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
      +    {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
      +    {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
      +    {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
      +    {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
      +    {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
      +    {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"},
      +    {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"},
      +    {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"},
      +    {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"},
      +    {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"},
      +    {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"},
      +    {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"},
      +    {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"},
      +    {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"},
      +    {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"},
      +    {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"},
      +    {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"},
      +    {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"},
      +    {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"},
      +    {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"},
      +    {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"},
      +    {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"},
      +    {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"},
      +    {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"},
      +    {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"},
      +    {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"},
      +    {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"},
      +    {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"},
      +    {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"},
      +    {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"},
      +    {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"},
      +    {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"},
      +    {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"},
      +    {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"},
      +    {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"},
      +    {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"},
      +    {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"},
      +    {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"},
      +    {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"},
      +    {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"},
      +    {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
      +    {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
      +    {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
      +    {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
      +    {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
      +    {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
      +    {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"},
      +    {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"},
      +    {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"},
      +    {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"},
      +    {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
      +    {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
      +    {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
      +    {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
      +    {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
      +    {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
      +    {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
      +    {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
      +    {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
      +    {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
      +    {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
      +    {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
      +    {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
      +    {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
      +    {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
      +    {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
      +]
      +
      +[package.extras]
      +cssselect = ["cssselect (>=0.7)"]
      +html5 = ["html5lib"]
      +htmlsoup = ["BeautifulSoup4"]
      +source = ["Cython (>=0.29.35)"]
      +
      +[[package]]
      +name = "markdown"
      +version = "3.4.4"
      +description = "Python implementation of John Gruber's Markdown."
      +optional = false
      +python-versions = ">=3.7"
      +files = [
      +    {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"},
      +    {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"},
      +]
      +
      +[package.dependencies]
      +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
      +
      +[package.extras]
      +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"]
      +testing = ["coverage", "pyyaml"]
      +
       [[package]]
       name = "markdown-it-py"
       version = "3.0.0"
       description = "Python port of markdown-it. Markdown parsing, done right!"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1402,7 +1496,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
       name = "markupsafe"
       version = "2.1.3"
       description = "Safely add untrusted strings to HTML/XML markup."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1462,7 +1555,6 @@ files = [
       name = "matplotlib-inline"
       version = "0.1.6"
       description = "Inline Matplotlib backend for Jupyter"
      -category = "dev"
       optional = false
       python-versions = ">=3.5"
       files = [
      @@ -1477,7 +1569,6 @@ traitlets = "*"
       name = "mccabe"
       version = "0.6.1"
       description = "McCabe checker, plugin for flake8"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -1489,7 +1580,6 @@ files = [
       name = "mdurl"
       version = "0.1.2"
       description = "Markdown URL utilities"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1501,7 +1591,6 @@ files = [
       name = "mistune"
       version = "3.0.1"
       description = "A sane and fast Markdown parser with useful plugins and renderers"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1513,7 +1602,6 @@ files = [
       name = "more-itertools"
       version = "9.1.0"
       description = "More routines for operating on iterables, beyond itertools"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1525,7 +1613,6 @@ files = [
       name = "mypy"
       version = "0.981"
       description = "Optional static typing for Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1569,7 +1656,6 @@ reports = ["lxml"]
       name = "mypy-extensions"
       version = "1.0.0"
       description = "Type system extensions for programs checked with the mypy type checker."
      -category = "dev"
       optional = false
       python-versions = ">=3.5"
       files = [
      @@ -1581,7 +1667,6 @@ files = [
       name = "mypy-protobuf"
       version = "3.4.0"
       description = "Generate mypy stub files from protobuf specs"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1597,7 +1682,6 @@ types-protobuf = ">=3.20.4"
       name = "nbclassic"
       version = "1.0.0"
       description = "Jupyter Notebook as a Jupyter Server extension."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1633,7 +1717,6 @@ test = ["coverage", "nbval", "pytest", "pytest-cov", "pytest-jupyter", "pytest-p
       name = "nbclient"
       version = "0.8.0"
       description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
      -category = "dev"
       optional = false
       python-versions = ">=3.8.0"
       files = [
      @@ -1643,7 +1726,7 @@ files = [
       
       [package.dependencies]
       jupyter-client = ">=6.1.12"
      -jupyter-core = ">=4.12,<5.0.0 || >=5.1.0"
      +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
       nbformat = ">=5.1"
       traitlets = ">=5.4"
       
      @@ -1656,7 +1739,6 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=
       name = "nbconvert"
       version = "7.6.0"
       description = "Converting Jupyter Notebooks"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1695,7 +1777,6 @@ webpdf = ["pyppeteer (>=1,<1.1)"]
       name = "nbformat"
       version = "5.9.0"
       description = "The Jupyter Notebook format"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1717,7 +1798,6 @@ test = ["pep440", "pre-commit", "pytest", "testpath"]
       name = "nest-asyncio"
       version = "1.5.6"
       description = "Patch asyncio to allow nested event loops"
      -category = "dev"
       optional = false
       python-versions = ">=3.5"
       files = [
      @@ -1729,7 +1809,6 @@ files = [
       name = "notebook"
       version = "6.5.4"
       description = "A web-based notebook environment for interactive computing"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1764,7 +1843,6 @@ test = ["coverage", "nbval", "pytest", "pytest-cov", "requests", "requests-unixs
       name = "notebook-shim"
       version = "0.2.3"
       description = "A shim layer for notebook traits and config"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1782,7 +1860,6 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"
       name = "numpy"
       version = "1.24.4"
       description = "Fundamental package for array computing in Python"
      -category = "main"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1820,7 +1897,6 @@ files = [
       name = "overrides"
       version = "7.3.1"
       description = "A decorator to automatically detect mismatch when overriding a method."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -1832,7 +1908,6 @@ files = [
       name = "packaging"
       version = "23.1"
       description = "Core utilities for Python packages"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1844,7 +1919,6 @@ files = [
       name = "pandas"
       version = "1.5.3"
       description = "Powerful data structures for data analysis, time series, and statistics"
      -category = "main"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -1893,7 +1967,6 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
       name = "pandocfilters"
       version = "1.5.0"
       description = "Utilities for writing pandoc filters in python"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
       files = [
      @@ -1905,7 +1978,6 @@ files = [
       name = "parso"
       version = "0.8.3"
       description = "A Python Parser"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -1921,7 +1993,6 @@ testing = ["docopt", "pytest (<6.0.0)"]
       name = "pathspec"
       version = "0.11.1"
       description = "Utility library for gitignore style pattern matching of file paths."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -1933,7 +2004,6 @@ files = [
       name = "pep517"
       version = "0.13.0"
       description = "Wrappers to build Python packages using PEP 517 hooks"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -1948,7 +2018,6 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
       name = "pexpect"
       version = "4.8.0"
       description = "Pexpect allows easy control of interactive console applications."
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -1963,7 +2032,6 @@ ptyprocess = ">=0.5"
       name = "pickleshare"
       version = "0.7.5"
       description = "Tiny 'shelve'-like database with concurrency support"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -1975,7 +2043,6 @@ files = [
       name = "pkginfo"
       version = "1.9.6"
       description = "Query metadata from sdists / bdists / installed packages."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -1990,7 +2057,6 @@ testing = ["pytest", "pytest-cov"]
       name = "pkgutil-resolve-name"
       version = "1.3.10"
       description = "Resolve a name to an object."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2002,7 +2068,6 @@ files = [
       name = "platformdirs"
       version = "3.8.0"
       description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2018,7 +2083,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-
       name = "pluggy"
       version = "1.2.0"
       description = "plugin and hook calling mechanisms for python"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2034,7 +2098,6 @@ testing = ["pytest", "pytest-benchmark"]
       name = "prometheus-client"
       version = "0.17.0"
       description = "Python client for the Prometheus monitoring system."
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2049,7 +2112,6 @@ twisted = ["twisted"]
       name = "prompt-toolkit"
       version = "3.0.39"
       description = "Library for building powerful interactive command lines in Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.7.0"
       files = [
      @@ -2064,7 +2126,6 @@ wcwidth = "*"
       name = "protobuf"
       version = "4.23.3"
       description = ""
      -category = "main"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2087,7 +2148,6 @@ files = [
       name = "psutil"
       version = "5.9.5"
       description = "Cross-platform lib for process and system monitoring in Python."
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
       files = [
      @@ -2114,7 +2174,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
       name = "ptyprocess"
       version = "0.7.0"
       description = "Run a subprocess in a pseudo terminal"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -2126,7 +2185,6 @@ files = [
       name = "pure-eval"
       version = "0.2.2"
       description = "Safely evaluate AST nodes without side effects"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -2141,7 +2199,6 @@ tests = ["pytest"]
       name = "pyarrow"
       version = "10.0.1"
       description = "Python library for Apache Arrow"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2179,7 +2236,6 @@ numpy = ">=1.16.6"
       name = "pycodestyle"
       version = "2.8.0"
       description = "Python style guide checker"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
       files = [
      @@ -2191,7 +2247,6 @@ files = [
       name = "pycparser"
       version = "2.21"
       description = "C parser in Python"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
       files = [
      @@ -2203,7 +2258,6 @@ files = [
       name = "pyflakes"
       version = "2.4.0"
       description = "passive checker of Python programs"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
       files = [
      @@ -2215,7 +2269,6 @@ files = [
       name = "pygments"
       version = "2.15.1"
       description = "Pygments is a syntax highlighting package written in Python."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2226,11 +2279,30 @@ files = [
       [package.extras]
       plugins = ["importlib-metadata"]
       
      +[[package]]
      +name = "pyspelling"
      +version = "2.8.2"
      +description = "Spell checker."
      +optional = false
      +python-versions = ">=3.6"
      +files = [
      +    {file = "pyspelling-2.8.2-py3-none-any.whl", hash = "sha256:ced1bc7c24c183801c6e529dac7d62fdb4c32e3f156740804900d7294fd504f9"},
      +    {file = "pyspelling-2.8.2.tar.gz", hash = "sha256:823afba99a77de58c1fc53b3cac712bb2335dacee410a1a14d150e94d3439f3e"},
      +]
      +
      +[package.dependencies]
      +beautifulsoup4 = "*"
      +html5lib = "*"
      +lxml = "*"
      +markdown = "*"
      +pyyaml = "*"
      +soupsieve = ">=1.8"
      +wcmatch = ">=6.0.3"
      +
       [[package]]
       name = "pytest"
       version = "7.4.0"
       description = "pytest: simple powerful testing with Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2253,7 +2325,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
       name = "pytest-mock"
       version = "3.11.1"
       description = "Thin-wrapper around the mock package for easier use with pytest"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2271,7 +2342,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
       name = "python-dateutil"
       version = "2.8.2"
       description = "Extensions to the standard Python datetime module"
      -category = "main"
       optional = false
       python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
       files = [
      @@ -2286,7 +2356,6 @@ six = ">=1.5"
       name = "python-json-logger"
       version = "2.0.7"
       description = "A python library adding a json log formatter"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2298,7 +2367,6 @@ files = [
       name = "pytz"
       version = "2023.3"
       description = "World timezone definitions, modern and historical"
      -category = "main"
       optional = false
       python-versions = "*"
       files = [
      @@ -2310,7 +2378,6 @@ files = [
       name = "pywin32"
       version = "306"
       description = "Python for Window Extensions"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -2334,7 +2401,6 @@ files = [
       name = "pywin32-ctypes"
       version = "0.2.2"
       description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2346,7 +2412,6 @@ files = [
       name = "pywinpty"
       version = "2.0.10"
       description = "Pseudo terminal support for Windows from Python."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2362,7 +2427,6 @@ files = [
       name = "pyyaml"
       version = "6.0"
       description = "YAML parser and emitter for Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2412,7 +2476,6 @@ files = [
       name = "pyzmq"
       version = "25.1.0"
       description = "Python bindings for 0MQ"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2502,7 +2565,6 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
       name = "readme-renderer"
       version = "40.0"
       description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -2522,7 +2584,6 @@ md = ["cmarkgfm (>=0.8.0)"]
       name = "referencing"
       version = "0.29.1"
       description = "JSON Referencing + Python"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -2538,7 +2599,6 @@ rpds-py = ">=0.7.0"
       name = "requests"
       version = "2.31.0"
       description = "Python HTTP for Humans."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2560,7 +2620,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
       name = "requests-toolbelt"
       version = "1.0.0"
       description = "A utility belt for advanced users of python-requests"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
       files = [
      @@ -2575,7 +2634,6 @@ requests = ">=2.0.1,<3.0.0"
       name = "rfc3339-validator"
       version = "0.1.4"
       description = "A pure python RFC3339 validator"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
       files = [
      @@ -2590,7 +2648,6 @@ six = "*"
       name = "rfc3986"
       version = "2.0.0"
       description = "Validating URI References per RFC 3986"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2605,7 +2662,6 @@ idna2008 = ["idna"]
       name = "rfc3986-validator"
       version = "0.1.1"
       description = "Pure python rfc3986 validator"
      -category = "dev"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
       files = [
      @@ -2617,7 +2673,6 @@ files = [
       name = "rich"
       version = "13.4.2"
       description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
      -category = "dev"
       optional = false
       python-versions = ">=3.7.0"
       files = [
      @@ -2637,7 +2692,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
       name = "rpds-py"
       version = "0.8.8"
       description = "Python bindings to Rust's persistent data structures (rpds)"
      -category = "dev"
       optional = false
       python-versions = ">=3.8"
       files = [
      @@ -2667,6 +2721,17 @@ files = [
           {file = "rpds_py-0.8.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:78c5577f99d2edc9eed9ec39fae27b73d04d1b2462aff6f6b11207e0364fc40d"},
           {file = "rpds_py-0.8.8-cp311-none-win32.whl", hash = "sha256:42eb3030665ee7a5c03fd4db6b8db1983aa91bcdffbed0f4687751deb2a94a7c"},
           {file = "rpds_py-0.8.8-cp311-none-win_amd64.whl", hash = "sha256:7110854662ccf8db84b90e4624301ef5311cafff7e5f2a63f2d7cc0fc1a75b60"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:d00b16de3c42bb3d26341b443e48d67d444bb1a4ce6b44dd5600def2da759599"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d7e540e4f85c04706ea798f47a86483f3d85c624704413bc701eb75684d35a5"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54a54c3c220e7c5038207912aab23443f829762503a4fcbc5c7bbffef7523b13"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22d5bef6f9942e46582610a60b8420f8e9af7e0c69e35c317cb508c30117f933"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fccd5e138908ae6f2db5fbfc6769e65372993b0c4c047586de15b6c31a76e8"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:502f0bee154fa1c13514dfddb402ef29b86aca11873a3316de4534cf0e13a1e8"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cfd2c2dbb0446ec1ba132e62e1f4880163e43e131dd43f58f58fd46430649b"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f12e679f29a6c2c0607b7037e7fce4f6430a0d304770768cf6d8036386918c29"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8f9d619c66dc7c018a22a1795a14ab4dad3c76246c9059b681955254a0f58f7c"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f9e70c04cc0402f8b14fec8ac91d1b825ac89a9aa015556a0af12a06b5f085"},
      +    {file = "rpds_py-0.8.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d72757c0cb6423fe73ecaa2db3adf0077da513b7fe8cb19e102de6df4ccdad0c"},
           {file = "rpds_py-0.8.8-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:d7e46f52272ceecc42c05ad869b068b2dbfb6eb5643bcccecd2327d3cded5a2e"},
           {file = "rpds_py-0.8.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7628b2080538faa4a1243b0002678cae7111af68ae7b5aa6cd8526762cace868"},
           {file = "rpds_py-0.8.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657348b35a4c2e7c2340bf0bc37597900037bd87e9db7e6282711aaa77256e16"},
      @@ -2693,6 +2758,17 @@ files = [
           {file = "rpds_py-0.8.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1891903e567d728175c0475a1f0ffc1d1580013b0b265b9e2f1b8c93d58b2d05"},
           {file = "rpds_py-0.8.8-cp39-none-win32.whl", hash = "sha256:ee42ce4ef46ea334ce8ab63d5a57c7fd78238c9c7293b3caa6dfedf11bd28773"},
           {file = "rpds_py-0.8.8-cp39-none-win_amd64.whl", hash = "sha256:0e8da63b9baa154ec9ddd6dd397893830d17e5812ceb50edbae8122d8ecb9f2e"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ecc79cd61c4c16f92521c7d34e0f534bc486fc5ed5d1fdf8d4e6e0c578dc7e07"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d43e4253469a6149f4dae91189ccbf832dcd870109b940fa6acb02769e57802b"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9985927f001d98d38ad90e0829d3e3c162ce42060bafb833782a934bf1d1d39b"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7af604c6581da01fd5f89a917c903a324779fdfa7b3ae66204865d34b5f2c502"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53ed81e3a9a0eb3dfd404ee097d4f50ec6985301ea6e03b4d57cd7ef239179f9"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6b93a8a17e84a53fa6636037955ba8e795f6645dab5ccbeb356c8dbc9cb371"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:592b9de8d82e919ffbf069e586808f56118a7f522bb0d018c54fa3526e5f2bed"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af0920c26fe3421f9e113975c185f7c42a3f0a8ead72cee5b4e6648af5d8cecc"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:28ce85916d7377b9734b280872fb456aa048712901edff9d60836c7b2e177265"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:cbbb26ac4dade6fdec22cb1155ca38d270b308f57cfd48a13a7a8ecc79369e82"},
      +    {file = "rpds_py-0.8.8-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8469755965ff2aa1da08e6e0afcde08950ebba84a4836cdc1672d097c62ffdbd"},
           {file = "rpds_py-0.8.8-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:f1c84912d77b01651488bbe392df593b4c5852e213477e268ebbb7c799059d78"},
           {file = "rpds_py-0.8.8-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:180963bb3e1fcc6ed6313ece5e065f0df4021a7eb7016084d3cbc90cd2a8af3e"},
           {file = "rpds_py-0.8.8-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e1251d6690f356a305192089017da83999cded1d7e2405660d14c1dff976af7"},
      @@ -2722,7 +2798,6 @@ files = [
       name = "secretstorage"
       version = "3.3.3"
       description = "Python bindings to FreeDesktop.org Secret Service API"
      -category = "dev"
       optional = false
       python-versions = ">=3.6"
       files = [
      @@ -2738,7 +2813,6 @@ jeepney = ">=0.6"
       name = "send2trash"
       version = "1.8.2"
       description = "Send file to trash natively under Mac OS X, Windows and Linux"
      -category = "dev"
       optional = false
       python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
       files = [
      @@ -2755,7 +2829,6 @@ win32 = ["pywin32"]
       name = "six"
       version = "1.16.0"
       description = "Python 2 and 3 compatibility utilities"
      -category = "main"
       optional = false
       python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
       files = [
      @@ -2767,7 +2840,6 @@ files = [
       name = "sniffio"
       version = "1.3.0"
       description = "Sniff out which async library your code is running under"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2779,7 +2851,6 @@ files = [
       name = "soupsieve"
       version = "2.4.1"
       description = "A modern CSS selector implementation for Beautiful Soup."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2791,7 +2862,6 @@ files = [
       name = "stack-data"
       version = "0.6.2"
       description = "Extract data from python stack frames and tracebacks for informative displays"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -2811,7 +2881,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
       name = "terminado"
       version = "0.17.1"
       description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2832,7 +2901,6 @@ test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"]
       name = "tinycss2"
       version = "1.2.1"
       description = "A tiny CSS parser"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2851,7 +2919,6 @@ test = ["flake8", "isort", "pytest"]
       name = "toml"
       version = "0.10.2"
       description = "Python Library for Tom's Obvious, Minimal Language"
      -category = "dev"
       optional = false
       python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
       files = [
      @@ -2863,7 +2930,6 @@ files = [
       name = "tomli"
       version = "2.0.1"
       description = "A lil' TOML parser"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2875,7 +2941,6 @@ files = [
       name = "tornado"
       version = "6.3.2"
       description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
      -category = "dev"
       optional = false
       python-versions = ">= 3.8"
       files = [
      @@ -2896,7 +2961,6 @@ files = [
       name = "traitlets"
       version = "5.9.0"
       description = "Traitlets Python configuration system"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2912,7 +2976,6 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
       name = "twine"
       version = "4.0.2"
       description = "Collection of utilities for publishing packages on PyPI"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2935,7 +2998,6 @@ urllib3 = ">=1.26.0"
       name = "types-protobuf"
       version = "4.23.0.1"
       description = "Typing stubs for protobuf"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -2947,7 +3009,6 @@ files = [
       name = "typing-extensions"
       version = "4.7.1"
       description = "Backported and Experimental Type Hints for Python 3.7+"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2959,7 +3020,6 @@ files = [
       name = "uri-template"
       version = "1.3.0"
       description = "RFC 6570 URI Template Processor"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2974,7 +3034,6 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake
       name = "urllib3"
       version = "2.0.3"
       description = "HTTP library with thread-safe connection pooling, file post, and more."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -2992,7 +3051,6 @@ zstd = ["zstandard (>=0.18.0)"]
       name = "vendoring"
       version = "1.2.0"
       description = "A command line tool, to simplify vendoring pure Python dependencies."
      -category = "dev"
       optional = false
       python-versions = "~= 3.8"
       files = [
      @@ -3012,11 +3070,24 @@ toml = "*"
       doc = ["sphinx"]
       test = ["pytest", "pytest-cov", "pytest-mock"]
       
      +[[package]]
      +name = "wcmatch"
      +version = "8.4.1"
      +description = "Wildcard/glob file name matcher."
      +optional = false
      +python-versions = ">=3.7"
      +files = [
      +    {file = "wcmatch-8.4.1-py3-none-any.whl", hash = "sha256:3476cd107aba7b25ba1d59406938a47dc7eec6cfd0ad09ff77193f21a964dee7"},
      +    {file = "wcmatch-8.4.1.tar.gz", hash = "sha256:b1f042a899ea4c458b7321da1b5e3331e3e0ec781583434de1301946ceadb943"},
      +]
      +
      +[package.dependencies]
      +bracex = ">=2.1.1"
      +
       [[package]]
       name = "wcwidth"
       version = "0.2.6"
       description = "Measures the displayed width of unicode strings in a terminal"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -3028,7 +3099,6 @@ files = [
       name = "webcolors"
       version = "1.13"
       description = "A library for working with the color formats defined by HTML and CSS."
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -3044,7 +3114,6 @@ tests = ["pytest", "pytest-cov"]
       name = "webencodings"
       version = "0.5.1"
       description = "Character encoding aliases for legacy web content"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -3056,7 +3125,6 @@ files = [
       name = "websocket-client"
       version = "1.6.1"
       description = "WebSocket client for Python with low level API options"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -3073,7 +3141,6 @@ test = ["websockets"]
       name = "y-py"
       version = "0.5.9"
       description = "Python bindings for the Y-CRDT built from yrs (Rust)"
      -category = "dev"
       optional = false
       python-versions = "*"
       files = [
      @@ -3149,7 +3216,6 @@ files = [
       name = "ypy-websocket"
       version = "0.8.2"
       description = "WebSocket connector for Ypy"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -3169,7 +3235,6 @@ test = ["mypy", "pre-commit", "pytest", "pytest-asyncio", "websockets (>=10.0)"]
       name = "zipp"
       version = "3.15.0"
       description = "Backport of pathlib-compatible object wrapper for zip files"
      -category = "dev"
       optional = false
       python-versions = ">=3.7"
       files = [
      @@ -3184,4 +3249,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
       [metadata]
       lock-version = "2.0"
       python-versions = "^3.8"
      -content-hash = "4ee90f2d1717ee09c85d7434a41e60b83e3fbdf65c1415b064702d5c8150e374"
      +content-hash = "0828e052604b6ccdbab589cf22ab2751d84b8596ae34ca86ecbe322eccda10e1"
      diff --git a/pyproject.toml b/pyproject.toml
      index c29a7bfe8..361b350b0 100644
      --- a/pyproject.toml
      +++ b/pyproject.toml
      @@ -25,6 +25,7 @@ jupyterlab = "^3.6.3"
       pytest-mock = "^3.9.0"
       pytest = "^7.1.3"
       twine = "^4.0.0"
      +pyspelling = "^2.8.2"
       
       [build-system]
       requires = ["poetry-core>=1.0.0"]
      
      From 41f8ba67df42518dc601493120959df5bf598fb6 Mon Sep 17 00:00:00 2001
      From: David WF 
      Date: Mon, 21 Aug 2023 16:50:50 -0700
      Subject: [PATCH 6/7] Use sudo
      
      ---
       .github/workflows/python_lint.yml | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml
      index 91dd15dbb..9d8e212f4 100644
      --- a/.github/workflows/python_lint.yml
      +++ b/.github/workflows/python_lint.yml
      @@ -14,7 +14,7 @@ jobs:
               with:
                 python-version: 3.11
             - name: Install aspell for pyspelling
      -        run: apt-get install -y aspell
      +        run: sudo apt-get install -y aspell
             - name: Upgrade pip
               run: pip install --upgrade pip
             - name: Install packages
      
      From cd6255a8283bdf7d8b025ba50e01cade4e3ccaba Mon Sep 17 00:00:00 2001
      From: David WF 
      Date: Mon, 21 Aug 2023 16:54:29 -0700
      Subject: [PATCH 7/7] Move spellcheck to linting, case-insensitve sort for
       wordlist
      
      ---
       .github/workflows/spellcheck.yml |  11 ---
       .wordlist.txt                    | 150 +++++++++++++++----------------
       2 files changed, 75 insertions(+), 86 deletions(-)
       delete mode 100644 .github/workflows/spellcheck.yml
      
      diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
      deleted file mode 100644
      index d67ca0e89..000000000
      --- a/.github/workflows/spellcheck.yml
      +++ /dev/null
      @@ -1,11 +0,0 @@
      -name: Spellcheck
      -on: [pull_request]
      -jobs:
      -  checks:
      -    runs-on: ubuntu-latest
      -    name: Spellcheck
      -    steps:
      -      - name: Check out source repository
      -        uses: actions/checkout@v3
      -      - uses: rojopolis/spellcheck-github-actions@v0
      -        name: Spellcheck
      diff --git a/.wordlist.txt b/.wordlist.txt
      index 28045233b..3f1d114ce 100644
      --- a/.wordlist.txt
      +++ b/.wordlist.txt
      @@ -1,85 +1,13 @@
      -APIs
      -AST
       AdministratorAccess
      -Avro
      -CIDR
      -DDL
      -DSL
      -DSLs
      -DataFrame
      -DataFrames
      -Datadog
      -Dockerfile
      -Flink
      -GCP
      -GCP's
      -GRPC
      -Github
      -Grafana
      -Graviton
      -Groupby
      -IOPS
      -InfoSec
      -Instacart
      -JSON
      -JSX
      -JVM
      -Kaggle
      -Kubernetes
      -LHS
      -LastK
      -MockClient
      -Nones
      -OAuth
      -OOM
      -OpenSSL's
      -PII
      -PLAINTEXT
      -PagerDuty
      -PoolableConnectionFactory
      -PrivateLink
      -Pulumi
      -PyO
      -Pydantic
      -RHS
      -ROI
      -RPCs
      -Realtimeliness
      -RocksDB
      -SASL
      -SDK
      -SHA
      -SLA
      -SSL
      -SearchRequest
      -Signifier
      -Stddev
      -TLS
      -TLSv
      -TestCase
      -TestDataset
      -Tokio
      -Tokio's
      -UI
      -UserCreator
      -UserCreditScore
      -UserFeature
      -UserFeatures
      -UserInfo
      -UserInfoDataset
      -UserLocation
      -UserPost
      -UserTransactionsAbroad
      -VPC
      -WIP
      -WIP
      -YAML
       ai
       api
      +APIs
       architected
       assertEqual
      +AST
       async
       autoscaling
      +Avro
       backend
       backfill
       backfilled
      @@ -88,6 +16,7 @@ bmr
       bool
       boolean
       booleans
      +CIDR
       classmethod
       classmethods
       codebase
      @@ -97,8 +26,11 @@ config
       configs
       csv
       dataclass
      +Datadog
       dataflow
      +DataFrame
       dataframe
      +DataFrames
       dataset
       dataset's
       datasets
      @@ -106,75 +38,130 @@ datastore
       datastores
       datetime
       dateutil
      +DDL
       declaratively
       dedup
       denormalize
       dev
       df
       dfe
      +Dockerfile
       docsnip
       ds
      +DSL
      +DSLs
       durations
       embeddings
       enabledTLSProtocols
       featureset
       featuresets
       fintech
      +Flink
       frontend
      +GCP
      +GCP's
       geocoding
       geoid
      +Github
      +Grafana
      +Graviton
      +Groupby
       groupby
      +GRPC
       gserviceaccount
       hackathon
       hardcoded
       html
       hudi
       iam
      +InfoSec
      +Instacart
      +IOPS
       ip
       ish
       ith
       jdbc
      +JSON
       json
      +JSX
      +JVM
       kafka
      +Kaggle
      +Kubernetes
       kwarg
       kwargs
      +LastK
       latencies
      +LHS
       lifecycle
       lookup
       lookups
       metaflags
      +MockClient
       multicolumn
       mysql
       nan
       natively
      +Nones
       noqa
       np
       nullable
      +OAuth
      +OOM
      +OpenSSL's
      +PagerDuty
       params
       parseable
       pid
      +PII
      +PLAINTEXT
      +PoolableConnectionFactory
       postgres
       pre
       precompute
       precomputed
      +PrivateLink
       protobuf
       protobufs
      +Pulumi
      +Pydantic
      +PyO
       quickstart
       realtime
      +Realtimeliness
       regex
       regexes
       repo
      +RHS
      +RocksDB
      +ROI
      +RPCs
       runtime
      +SASL
       scalability
       scalable
       schemas
      +SDK
      +SearchRequest
      +SHA
      +Signifier
       signup
      +SLA
       snowflakecomputing
      +SSL
       stateful
      +Stddev
       str
       strftime
       struct
      +TestCase
      +TestDataset
       tiering
      +TLS
      +TLSv
      +Tokio
      +Tokio's
      +UI
       uid
       uint
       uints
      @@ -182,6 +169,19 @@ uncomment
       unittest
       uptime
       uptimes
      +UserCreator
      +UserCreditScore
      +UserFeature
      +UserFeatures
       userid
      +UserInfo
      +UserInfoDataset
      +UserLocation
      +UserPost
      +UserTransactionsAbroad
      +VPC
       webhook
       webhooks
      +WIP
      +WIP
      +YAML